MCP Server + Claude CLI na serwerze — jak przenieść AI narzędzia z laptopa na własną infrastrukturę

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.

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Przewijanie do góry