Jeśli nie wiesz jeszcze czym jest MCP — zacznij od mojego poprzedniego artykułu, w którym tłumaczę od podstaw czym jest Model Context Protocol i jak działa z Claude Desktop. Ten wpis jest bezpośrednią kontynuacją tamtego tematu.
Jeśli używasz Claude Desktop z lokalnymi serwerami MCP, pewnie już wiesz jak potężne jest to połączenie. Ale co jeśli powiem Ci, że możesz to wszystko przenieść na serwer — i zyskać automatyzację, dostęp z każdego miejsca oraz stabilność, której nie zapewni żaden laptop?
W tym artykule przeprowadzę Cię krok po kroku przez wdrożenie serwera MCP na własnej VM, podłączenie go do Claude CLI i skonfigurowanie całości tak, żeby działało 24/7 jako usługa systemowa.
Po co MCP na serwerze?
Standardowy setup to Claude Desktop + lokalny serwer MCP napisany w Pythonie, działający na Twoim komputerze. Działa świetnie — dopóki nie natkniesz się na jego ograniczenia:
- Lokalny MCP pada razem z laptopem. Zamykasz pokrywę — serwer ginie. Żadnych zadań w tle, żadnej automatyzacji.
- Jeden użytkownik. Lokalny serwer obsługuje tylko Ciebie, na jednej maszynie.
- Trudna automatyzacja. Uruchomienie skryptu który woła Claude z narzędziami MCP o 3 w nocy? Na lokalnym setupie — karkołomne.
- Brak dostępu do zasobów sieciowych. Serwer na VM ma naturalny dostęp do baz danych, wewnętrznych API i innych maszyn w sieci.
Przeniesienie MCP na serwer rozwiązuje wszystkie te problemy. Dostajesz stabilną usługę, dostęp przez SSE/HTTP z dowolnego miejsca, możliwość podpięcia Claude CLI do automatycznych pipeline’ów i pełną kontrolę nad środowiskiem.
Claude CLI — co już ma wbudowane?
Zanim przejdziemy do pisania własnych narzędzi MCP, warto wiedzieć czego nie musisz pisać, bo Claude CLI (Claude Code) ma to wbudowane od razu:
- Bash — wykonywanie dowolnych komend powłoki
- Read / Write / Edit — odczyt, zapis i edycja plików
- Git — operacje na repozytoriach (commit, diff, log, blame…)
- Glob / Grep — przeszukiwanie kodu i plików po wzorcach
- Web Search — wyszukiwanie w internecie
- Subagenci — delegowanie podzadań do wyspecjalizowanych instancji Claude
Oznacza to, że narzędzia MCP mają sens tylko wtedy, gdy dają Claude dostęp do czegoś czego sam nie może zrobić — czyli do Twoich wewnętrznych systemów, baz danych, prywatnych API czy serwisów wymagających uwierzytelnienia. Pisanie narzędzia MCP które robi ping albo wyświetla free -h to powielanie tego co Claude CLI robi bez żadnych narzędzi.
Czego użyjemy
- Ubuntu 22.04 LTS — system na VM
- FastMCP 3.x — biblioteka Python do pisania serwerów MCP, transport SSE
- systemd — zarządzanie usługą (auto-start, restart po awarii)
- nginx — reverse proxy z TLS
- Claude CLI (Claude Code) — klient Claude w terminalu
Architektura
Zanim wejdziemy w kod, warto zrozumieć jak to działa:
Lokalnie (stary setup):
Claude Desktop → stdio → python mcp_server.py (tylko Twój komputer)
Na serwerze (nowy setup):
Claude CLI / skrypty bash / cron / CI-CD
↓ HTTP SSE
FastMCP server (systemd, port XXXX)
↓
Twoje zasoby: bazy danych, wewnętrzne API, infrastruktura...
Kluczowa zmiana to transport — zamiast stdio (lokalny proces) używamy SSE (Server-Sent Events), czyli trwałego połączenia HTTP przez które serwer przesyła eventy do klienta. MCP staje się normalnym serwisem sieciowym.
Krok 1 — Przygotowanie serwera
Potrzebujesz VM lub serwera z Ubuntu 22.04 (lub nowszym). Minimalne wymagania to 1-2 vCPU i 2 GB RAM — serwer MCP jest lekki. Po zalogowaniu zainstaluj zależności:
sudo apt update && sudo apt upgrade -y
sudo apt install -y python3-pip python3-venv python3-full nginx curl git
Krok 2 — Dedykowany użytkownik systemowy
Serwer MCP nie powinien działać jako root. Tworzę dedykowanego użytkownika bez powłoki logowania:
sudo useradd -r -s /bin/bash -m -d /opt/mcp mcp
Wszystkie pliki serwera trafią do /opt/mcp. To izoluje serwer od reszty systemu — nawet jeśli ktoś przez lukę w narzędziach MCP dostanie dostęp do procesu, będzie ograniczony do uprawnień użytkownika mcp.
Krok 3 — Virtualenv i FastMCP
sudo -u mcp python3 -m venv /opt/mcp/venv
sudo -u mcp /opt/mcp/venv/bin/pip install fastmcp
Weryfikacja (uruchamiamy z katalogu /opt/mcp, bo pydantic-settings szuka tam pliku .env):
sudo -u mcp bash -c "cd /opt/mcp && /opt/mcp/venv/bin/python -c \"import fastmcp; print(fastmcp.__version__)\""
Krok 4 — Serwer MCP z sensownymi narzędziami
Pamiętając że Claude CLI już obsługuje bash i pliki, piszę narzędzia które dają mu dostęp do rzeczy których sam nie ma: bazy danych, API infrastruktury i powiadomienia. Poniżej przykładowy /opt/mcp/server.py:
from fastmcp import FastMCP
import psycopg2, requests, os
mcp = FastMCP("moj-serwer-mcp")
# Zapytania do wewnętrznej bazy danych
# Claude CLI nie ma dostępu do Twojej bazy — MCP daje mu ten dostęp w kontrolowany sposób
@mcp.tool()
def query_database(sql: str) -> list:
"""Wykonuje zapytanie SELECT na wewnętrznej bazie danych"""
if not sql.strip().upper().startswith("SELECT"):
return [{"error": "Dozwolone tylko zapytania SELECT"}]
conn = psycopg2.connect(os.environ["DATABASE_URL"])
cur = conn.cursor()
cur.execute(sql)
cols = [d[0] for d in cur.description]
rows = [dict(zip(cols, row)) for row in cur.fetchall()]
conn.close()
return rows
# Status infrastruktury z wewnętrznego API
# Prywatne API niedostępne z zewnątrz bez tunelu
@mcp.tool()
def get_infrastructure_status() -> dict:
"""Pobiera status infrastruktury z wewnętrznego API"""
url = os.environ["INFRA_API_URL"] + "/status"
headers = {"Authorization": f"Bearer {os.environ['INFRA_API_TOKEN']}"}
r = requests.get(url, headers=headers, timeout=10)
return r.json()
# Wysyłanie powiadomień na Slack
# Claude CLI nie ma Twoich webhooków — MCP je enkapsuluje
@mcp.tool()
def notify_slack(message: str, channel: str = "general") -> str:
"""Wysyła powiadomienie na Slack"""
webhook = os.environ["SLACK_WEBHOOK_URL"]
r = requests.post(webhook, json={"text": message, "channel": f"#{channel}"}, timeout=5)
return "ok" if r.status_code == 200 else f"błąd: {r.status_code}"
# Metryki z systemu monitoringu
# Prometheus / Grafana API dostępne tylko wewnętrznie
@mcp.tool()
def get_metrics(query: str) -> dict:
"""Wykonuje zapytanie PromQL do wewnętrznego Prometheusa"""
url = os.environ["PROMETHEUS_URL"] + "/api/v1/query"
r = requests.get(url, params={"query": query}, timeout=10)
return r.json()
if __name__ == "__main__":
mcp.run(transport="sse", host="0.0.0.0", port=8785)
Każde z tych narzędzi daje Claude dostęp do zasobu którego nie miałby inaczej — wewnętrzna baza danych, prywatne API, firmowy Slack, wewnętrzny Prometheus. To właściwe zastosowanie MCP: nie powielanie bash, lecz integracja z Twoim ekosystemem.
Plik z sekretami /opt/mcp/.env — nigdy nie wpisuj ich bezpośrednio w kod:
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
INFRA_API_URL=http://10.0.0.10:8080
INFRA_API_TOKEN=tajny-token
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/xxx/yyy/zzz
PROMETHEUS_URL=http://monitoring.internal:9090
Uprawnienia tylko dla użytkownika mcp:
sudo chown mcp:mcp /opt/mcp/.env
sudo chmod 600 /opt/mcp/.env
Krok 5 — Sprawdź port i uruchom testowo
Przed uruchomieniem upewnij się że wybrany port jest wolny:
sudo ss -tlnp | grep 8785
Jeśli coś już tam siedzi — zmień port w server.py na inny. Następnie test na pierwszym planie:
sudo -u mcp bash -c "cd /opt/mcp && /opt/mcp/venv/bin/python server.py"
Powinieneś zobaczyć baner FastMCP i linię Application startup complete. Zatrzymaj (Ctrl+C) i przejdź do systemd.
Krok 6 — Usługa systemd
Tworzę plik /etc/systemd/system/mcp.service:
[Unit]
Description=MCP Server
After=network.target
[Service]
Type=simple
User=mcp
WorkingDirectory=/opt/mcp
ExecStart=/opt/mcp/venv/bin/python /opt/mcp/server.py
EnvironmentFile=/opt/mcp/.env
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable mcp
sudo systemctl start mcp
sudo systemctl status mcp
Od tej chwili serwer startuje automatycznie wraz z systemem i restartuje się po awarii. Logi w czasie rzeczywistym:
journalctl -u mcp -f
Krok 7 — Nginx jako reverse proxy z TLS
Port MCP powinien być dostępny tylko lokalnie. Na zewnątrz wystawiamy go przez nginx z TLS i uwierzytelnianiem tokenem:
server {
listen 443 ssl;
server_name mcp.twoja-domena.pl;
ssl_certificate /etc/letsencrypt/live/mcp.twoja-domena.pl/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mcp.twoja-domena.pl/privkey.pem;
if ($http_x_mcp_token != "twoj-tajny-token") {
return 401;
}
location / {
proxy_pass http://127.0.0.1:8785;
proxy_set_header Connection '';
proxy_buffering off; # SSE wymaga wyłączonego bufora!
proxy_cache off;
chunked_transfer_encoding on;
}
}
Firewall — port MCP zablokowany z zewnątrz:
sudo ufw allow ssh
sudo ufw allow 443
sudo ufw deny 8785
sudo ufw enable
Krok 8 — Claude CLI
Instaluję Node.js 20 LTS i Claude CLI:
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
sudo npm install -g @anthropic-ai/claude-code
Konfiguracja dostępu — dwie opcje:
Opcja A — klucz API (konto na console.anthropic.com):
echo 'export ANTHROPIC_API_KEY="sk-ant-..."' >> ~/.bashrc
source ~/.bashrc
Opcja B — subskrypcja Claude.ai Pro lub Max (bez osobnego klucza API):
claude
# przy pierwszym uruchomieniu zaloguje Cię przez OAuth w przeglądarce
Podłączam MCP server:
claude mcp add moj-serwer http://localhost:8785/sse --transport sse
claude mcp list
# → moj-serwer: http://localhost:8785/sse (SSE) - ✓ Connected
Krok 9 — Pierwsze wywołanie
W trybie nieinteraktywnym (-p) Claude CLI domyślnie blokuje narzędzia MCP jako zabezpieczenie. Musisz jawnie wskazać które narzędzia są dozwolone:
# konkretne narzędzie
claude -p "Sprawdź status infrastruktury" --allowedTools "mcp__moj-serwer__get_infrastructure_status"
# wszystkie narzędzia z serwera
claude -p "Sprawdź metryki i wyślij raport na Slack" --allowedTools "mcp__moj-serwer__*"
To celowy design — w automatycznych skryptach zawsze musisz explicite określić jakie uprawnienia ma Claude. Dobra praktyka bezpieczeństwa, nie błąd.
Automatyzacja — przykłady
# cron — codzienny raport o 8:00
0 8 * * * claude -p "Pobierz metryki z Prometheusa i wyślij dzienny raport na Slack" \
--allowedTools "mcp__moj-serwer__*" >> /var/log/daily-report.log
# GitLab CI — powiadomienie po deploymencie
script:
- claude -p "Deployment zakończony, wyślij podsumowanie na Slack #releases" \
--allowedTools "mcp__moj-serwer__notify_slack"
Podsumowanie bezpieczeństwa
| Element | Rozwiązanie |
|---|---|
| Izolacja procesu | Dedykowany user mcp, nie root |
| Sekrety | Plik .env z chmod 600, nigdy w kodzie |
| Sieć | Port MCP tylko lokalnie, na zewnątrz nginx + TLS |
| Uwierzytelnienie | Token w nagłówku lub basic auth w nginx |
| Walidacja wejścia | Whitelist operacji w każdym narzędziu (np. tylko SELECT w DB) |
| Uprawnienia Claude | --allowedTools w skryptach — zawsze explicite |
| Logi | journald — pełna historia wywołań |
Pełna dokumentacja Claude CLI dostępna jest na docs.anthropic.com/claude-code.
Wszystkie komendy testowałem na Ubuntu 22.04.2 LTS, FastMCP 3.1.1, Claude CLI (Claude Code) 2.1.85.