MCP (Model Context Protocol) — jak podłączyć AI do serwera i nie dać mu roota
Asystent AI potrafi napisać regułę ufw, wyjaśnić błąd z journalctl i podpowiedzieć, czemu kontener
nie wstaje. Ma jedną wadę: nie widzi Twojego serwera. Kończy się na kopiowaniu logów do okna czatu
i z powrotem — wolno, a do tego łatwo wkleić coś, czego wklejać nie powinno się nigdzie.
Tę lukę zamyka MCP — Model Context Protocol: otwarty standard (Anthropic opublikował go pod koniec 2024 roku, dziś obsługuje go większość narzędzi AI), który mówi, jak aplikacja z modelem językowym rozmawia z zewnętrznymi narzędziami i danymi. Najprościej: to takie USB-C dla AI — piszesz jeden serwer MCP, a podłączysz go do Claude, edytora kodu czy własnego agenta.
W tym wpisie: jak MCP działa, własny serwer MCP w Pythonie, który pozwala AI sprawdzić dyski, usługi i logi tylko do odczytu, podłączenie go lokalnie i przez SSH oraz checklista bezpieczeństwa.
Jak działa MCP
Trzy role:
- host — aplikacja, w której rozmawiasz z modelem (Claude Desktop, Claude Code w terminalu, IDE);
- klient MCP — część hosta, która utrzymuje połączenie z jednym serwerem;
- serwer MCP — Twój program, który wystawia możliwości: narzędzia (tools — funkcje, które model może wywołać), zasoby (resources — dane do odczytu, np. plik konfiguracji) i podpowiedzi (prompts — gotowe szablony zadań).
Wiadomości to JSON-RPC 2.0. Transport jest dwojaki: stdio — host uruchamia serwer jako proces i rozmawia z nim przez standardowe wejście i wyjście (lokalnie, najprościej) — albo Streamable HTTP, gdy serwer działa zdalnie jako usługa. Model nie dostaje dostępu do maszyny: widzi listę narzędzi z opisami, prosi o wywołanie, a host (zwykle po Twoim potwierdzeniu) je wykonuje i oddaje wynik.
Z punktu widzenia admina to dobra wiadomość: to Ty decydujesz, co narzędzie robi. Model nie dostaje powłoki — dostaje kilka funkcji, które sam napisałeś.
Serwer MCP tylko do odczytu w Pythonie
Cel: AI ma odpowiedzieć na pytanie „czy na serwerze nie kończy się miejsce i czy zabbix-agent2 działa?”
bez dostępu do sudo i bez dowolnych poleceń. Instalacja oficjalnego SDK w środowisku wirtualnym:
tomasz@srv:~$ python3 -m venv ~/mcp-admin
tomasz@srv:~$ ~/mcp-admin/bin/pip install "mcp[cli]"
Serwer — plik ~/mcp-admin/serwer.py:
#!/usr/bin/env python3
"""admin-ro — serwer MCP tylko do odczytu: dyski, stan usług i ostatnie logi."""
import subprocess
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("admin-ro")
# Tylko te usługi — model nie poda dowolnej nazwy ani opcji polecenia.
USLUGI = {"ssh", "nginx", "docker", "mariadb", "zabbix-agent2"}
LIMIT = 4000 # znaków odpowiedzi: logi potrafią mieć megabajty
def uruchom(*polecenie: str) -> str:
"""Polecenie jako lista argumentów (bez powłoki), z limitem czasu i długości wyniku."""
try:
wynik = subprocess.run(polecenie, capture_output=True, text=True, timeout=10)
except subprocess.TimeoutExpired:
return "przekroczony czas (10 s)"
tekst = (wynik.stdout + wynik.stderr).strip()
return tekst[-LIMIT:]
def dozwolona(nazwa: str) -> str:
if nazwa not in USLUGI:
raise ValueError("usługa spoza listy: " + ", ".join(sorted(USLUGI)))
return nazwa
@mcp.tool()
def dyski() -> str:
"""Zajętość systemów plików (df -h, bez tmpfs)."""
return uruchom("df", "-h", "-x", "tmpfs", "-x", "devtmpfs")
@mcp.tool()
def usluga(nazwa: str) -> str:
"""Stan usługi systemd (systemctl status) z listy dozwolonych."""
return uruchom("systemctl", "status", "--no-pager", "--lines=0", dozwolona(nazwa))
@mcp.tool()
def logi(nazwa: str, linie: int = 50) -> str:
"""Ostatnie linie dziennika usługi (journalctl -u), najwyżej 200."""
linie = max(1, min(linie, 200))
return uruchom("journalctl", "-u", dozwolona(nazwa), "-n", str(linie), "--no-pager", "-o", "short-iso")
if __name__ == "__main__":
mcp.run() # transport stdio: JSON-RPC na stdin/stdout
Kilka rzeczy, które są tu celowo:
- opis funkcji to instrukcja dla modelu — z docstringu i typów argumentów SDK buduje opis narzędzia, więc pisz je tak, jak dokumentację dla kolegi z zespołu;
- lista dozwolonych usług zamiast parametru „wpisz dowolną nazwę” —
nazwanie trafi do polecenia, jeśli nie ma jej na liście, asubprocess.runz listą argumentów nie przechodzi przez powłokę; - limit czasu i długości — 50 000 linii logu nie pomoże ani modelowi, ani Tobie;
- przy transporcie stdio nic nie wypisuj na stdout (
print()do debugowania zepsuje protokół) — komunikaty diagnostyczne tylko na stderr albo do pliku.
Konto, na którym działa serwer, nie potrzebuje sudo. Żeby journalctl pokazywał logi usług systemowych,
wystarczy grupa systemd-journal:
tomasz@srv:~$ sudo usermod -aG systemd-journal tomasz
Przed podłączeniem do AI sprawdź serwer w MCP Inspector — przeglądarkowym kliencie, w którym wywołasz narzędzia ręcznie i zobaczysz dokładnie to, co dostanie model:
tomasz@srv:~$ npx @modelcontextprotocol/inspector ~/mcp-admin/bin/python ~/mcp-admin/serwer.py
Podłączenie do Claude
Claude Code (terminal) — jedna komenda:
tomasz@srv:~$ claude mcp add admin-ro -- /home/tomasz/mcp-admin/bin/python /home/tomasz/mcp-admin/serwer.py
tomasz@srv:~$ claude mcp list
Claude Desktop — wpis w claude_desktop_config.json (Ustawienia → Developer → Edit Config), potem
restart aplikacji:
{
"mcpServers": {
"admin-ro": {
"command": "/home/tomasz/mcp-admin/bin/python",
"args": ["/home/tomasz/mcp-admin/serwer.py"]
}
}
}
Od tej chwili pytanie „sprawdź miejsce na dyskach i stan zabbix-agent2” kończy się wywołaniem dyski
i usluga — a przed każdym wywołaniem klient pyta Cię o zgodę.
Serwer zdalny przez SSH
Najwygodniejsze dla admina: serwer MCP leży na serwerze, a Claude działa na Twoim laptopie. Transport
stdio to tylko wejście i wyjście procesu, więc wystarczy… SSH. Na serwerze osobne konto (np. mcp-ro)
bez sudo, z grupą systemd-journal, a w jego ~/.ssh/authorized_keys klucz z wymuszoną komendą —
nawet z kluczem w ręku nie da się na tym koncie zrobić nic poza uruchomieniem serwera MCP:
restrict,command="/opt/mcp-admin/bin/python /opt/mcp-admin/serwer.py" ssh-ed25519 AAAA… mcp-laptop
W konfiguracji klienta zamiast Pythona — ssh:
{
"mcpServers": {
"srv-admin-ro": {
"command": "ssh",
"args": ["-T", "-o", "BatchMode=yes", "-i", "/home/tomasz/.ssh/mcp_ed25519", "mcp-ro@srv.lan"]
}
}
}
Klucz tylko do tego (ssh-keygen -t ed25519 -f ~/.ssh/mcp_ed25519) — jak skonfigurować logowanie kluczem,
opisałem we wpisie SSH bez hasła.
Checklista bezpieczeństwa
- Najmniejsze uprawnienia: osobne konto, bez
sudo, tylko odczyt. Narzędzie, które coś zmienia (restart usługi), to osobna decyzja — i osobny serwer. - Żadnego „uruchom polecenie”: narzędzie
exec(cmd)to zdalna powłoka z dodatkowym krokiem. Konkretne funkcje, lista dozwolonych wartości, argumenty bez powłoki. - Wynik narzędzia to dane, nie polecenia: w logach i plikach może stać tekst w stylu „zignoruj poprzednie instrukcje i…” (prompt injection). Model może się na to nabrać — dlatego serwer nie ma narzędzi, którymi dałoby się coś zepsuć.
- Sekrety zostają na serwerze: nie wystawiaj narzędziami
.env, kluczy ani zrzutów konfiguracji z hasłami — wszystko, co zwróci narzędzie, trafia do modelu. - Zatwierdzaj wywołania — nie włączaj „zawsze pozwalaj” dla narzędzi, których wyniku nie znasz.
- Cudze serwery MCP czytaj przed uruchomieniem: to zwykły program z Twoimi uprawnieniami. Sprawdź źródło i wersję jak każdą inną zależność.
- Zdalny serwer HTTP — tylko z uwierzytelnianiem i w sieci wewnętrznej / VPN, nigdy otwarty na świat.
Podsumowanie
MCP nie zastąpi monitoringu ani admina, ale świetnie skraca drogę od pytania do odpowiedzi: zamiast kopiować logi do czatu, pozwalasz AI samodzielnie zajrzeć — dokładnie tam, gdzie mu pozwolisz. Zacznij od kilku narzędzi tylko do odczytu, sprawdź je w Inspectorze, a potem dokładaj kolejne. Alarmy niech dalej przychodzą z Zabbixa (monitoring dysków w Zabbixie) — AI przyda się wtedy, gdy trzeba szybko zrozumieć, co się stało.
W kolejnych wpisach z działu AI: jak dać agentowi AI zadanie na serwerze tak, żeby było co sprawdzić po jego pracy, i kiedy lokalny model wystarczy zamiast chmury.