Hub MCP to jeden kontener, który uruchamia serwery MCP opisane w poprzednich częściach serii i udostępnia je Claude Code, Open WebUI i Windmillowi. Wpis opisuje konfigurację MCPHub (compose, sekrety w .env, izolacja procesów, grupy narzędzi, klucze per klient), przykłady użycia i problemy napotkane przy wdrożeniu.
Problem: każdy klient ma własne serwery MCP
Serwery z poprzednich części były konfigurowane osobno w każdym kliencie. Przy kilku klientach (Claude Code w kilku projektach, Claude Desktop, Open WebUI, skrypty Windmilla) taki układ szybko przestaje się skalować.
| Koszt | Na czym polega |
|---|---|
| Klucze API w wielu miejscach | Ten sam klucz Tavily czy Brave siedzi w ~/.claude.json, w konfiguracji Desktopa i w Open WebUI. Rotacja klucza to edycja kilku plików. |
| Procesy na stacji roboczej | Każdy serwer stdio (npx, uvx) to osobny proces uruchamiany przez każdego otwartego klienta. Przed przeniesieniem serwerów do huba pięć otwartych sesji dawało ok. 28 procesów node, 4 procesy python i 2,3 GB RAM. Po przeniesieniu zostało 10 procesów node i ok. 0,9 GB (lokalnie działa już tylko Playwright i integracja PyCharm). |
| Brak kontroli nad zakresem | Agent w repo bloga nie potrzebuje narzędzi do sieci domowej, a agent infrastruktury nie potrzebuje generatora obrazów. Bez centralnego miejsca każdy klient widzi wszystko, co ma skonfigurowane. |
| Koszt kontekstu | Klienci bez leniwego ładowania narzędzi (np. Open WebUI) wysyłają modelowi pełne schematy wszystkich narzędzi przy każdym zapytaniu. |
Rozwiązaniem jest hub MCP: jeden kontener, który uruchamia wszystkie serwery MCP, trzyma ich sekrety i wystawia je klientom jako jeden endpoint HTTP, z podziałem na grupy narzędzi i osobnym kluczem dla każdego klienta. Każdy podpięty serwer można dostosować: udostępnić lub ukryć wybrane narzędzia. Narzędzia mogą też być ładowane na żądanie: agent wie, że dane narzędzie istnieje, ale pełną definicję jego parametrów dostaje dopiero wtedy, gdy o nie zapyta. Oszczędza to kontekst, bo w jednej sesji rzadko potrzebne są wszystkie narzędzia. To ładowanie na żądanie zapewnia klient (np. Claude Code), a hub pozwala ograniczyć, co w ogóle trafia na listę.
Czym jest MCPHub
MCPHub to agregator serwerów MCP z panelem WWW. Hub sam jest klientem dla podpiętych serwerów i serwerem dla aplikacji AI:
| Funkcja | Co robi hub |
|---|---|
| Serwery stdio | uruchamia serwery npx i uvx jako procesy w kontenerze |
| Serwery zdalne | łączy się z serwerami sse i streamable-http, także z OAuth |
| Endpoint dla klientów | wystawia narzędzia przez Streamable HTTP pod adresem /mcp/<grupa> |
| Zarządzanie narzędziami | włącza i wyłącza pojedyncze narzędzia, nadpisuje ich opisy i ukrywa parametry |
| Autoryzacja klientów | klucze bearer ograniczone do wybranych grup, w konfiguracji zapisane tylko jako skrót SHA-256 |
| API REST | wszystko, co robi panel, jest dostępne także przez API |
U mnie o wyborze MCPHub zdecydował tryb plikowy: cała konfiguracja (serwery, grupy, klucze) leży w jednym pliku JSON, który można trzymać w gicie. Od wersji 1.1.12 plik nie zawiera już jawnych kluczy klientów, tylko ich skróty, więc trzymanie go w repozytorium jest bezpieczniejsze.
%%{init: {'themeVariables': {'fontSize': '13px'}, 'themeCSS': '.nodeLabel, .nodeLabel p, .edgeLabel, .edgeLabel p, .label foreignObject div { text-align: left !important; }'}}%%
graph LR
subgraph Klienci
CC[Claude Code<br/>projekty]
OW[Open WebUI]
WM[Windmill]
end
subgraph Kontener mcphub
HUB[MCPHub<br/>grupy + klucze]
S1[tavily<br/>brave<br/>firecrawl<br/>stdio]
S2[markitdown<br/>imagegen<br/>elevenlabs<br/>google-tasks<br/>stdio]
end
subgraph Usługi zdalne
C4[crawl4ai]
GR[graphiti]
C7[context7]
end
CC -->|Bearer key<br/>/mcp/claude_code<br/>/mcp/blog| HUB
OW -->|Bearer key<br/>/mcp/research<br/>/mcp/pamiec| HUB
WM -->|Bearer key<br/>/mcp/research| HUB
HUB --> S1
HUB --> S2
HUB -->|SSE| C4
HUB -->|streamable-http| GR
HUB -->|HTTPS| C7
Uruchomienie w Dockerze
services:
mcphub:
image: samanhappy/mcphub:1.2.2
container_name: mcphub
restart: unless-stopped
env_file: .env
environment:
NODE_ENV: production
PORT: "3000"
TRUST_PROXY: "1"
INSTALL_BASE_URL: http://mcphub.mini-1.lan
MCPHUB_SETTING_PATH: /app/settings/mcp_settings.json
volumes:
- /home/robert/data/mcphub/data:/app/data
- /mnt/nas-1/artefacts:/app/artefacts
- ./settings:/app/settings
- /home/robert/data/mcphub/npm:/root/.npm
- /home/robert/data/mcphub/uv:/root/.cache/uv
- ./bin/mcp-isolate:/usr/local/bin/mcp-isolate:ro
- /home/robert/data/mcphub/runner:/home/mcp
networks: [proxy]
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
deploy:
resources:
limits: { memory: 3G, cpus: "2.0" }
networks:
proxy:
external: true
Najważniejsze decyzje w tym pliku:
- Konfiguracja w gicie. Plik
mcp_settings.jsonjest częścią repozytorium konfiguracji. - Cache
npxiuvxna hoście. Po odtworzeniu kontenera pakiety nie są pobierane od nowa. - Katalog
artefactsna NAS. Serwery generujące pliki (obrazy, audio) zapisują wyniki na udziale NAS. - Klucze API poza gitem. Sekrety serwerów MCP leżą w pliku
.envobok pliku compose, który nie trafia do repozytorium. Wmcp_settings.jsonwystępują tylko jako odwołania${ZMIENNA}, które hub rozwija przy starcie serwera.
ADMIN_PASSWORD=***
JWT_SECRET=***
BRAVE_API_KEY=***
TAVILY_API_KEY=***
REPLICATE_API_TOKEN=***
FIRECRAWL_API_KEY=***
ELEVENLABS_API_KEY=***
GRAFANA_URL=***
GRAFANA_SERVICE_ACCOUNT_TOKEN=***
UNIFI_USERNAME=***
UNIFI_PASSWORD=***
CONTEXT7_API_KEY=***
LITELLM_MCPHUB_KEY=***
CRAWL4AI_API_TOKEN=***
WINDMILL_MCP_TOKEN=***
UNIFI_API_KEY=***
GOOGLE_OAUTH_CLIENT_ID=***
GOOGLE_OAUTH_CLIENT_SECRET=***
BAMBU_TOKEN=***
ADMIN_PASSWORD działa tylko przy pierwszym starcie, gdy w konfiguracji nie ma jeszcze użytkowników.
Izolacja serwerów stdio: mcp-isolate
Serwery stdio nie są uruchamiane bezpośrednio, tylko przez krótki wrapper montowany do kontenera. Powód jest prosty: MCPHub przekazuje każdemu serwerowi stdio całe środowisko huba. Serwer Tavily dostaje więc też klucz ElevenLabs, token Grafany i hasło do kontrolera sieci. Dodatkowo obraz działa jako root, więc każdy proces może przeczytać /proc/1/environ huba i plik z kluczami klientów. Skompromitowany pakiet npm albo PyPI miałby dostęp do wszystkiego.
#!/bin/bash
# mcp-isolate — uruchamia serwer stdio MCP z czystym srodowiskiem i (domyslnie) jako nobody.
set -euo pipefail
as_root=0
if [[ ${1-} == --root ]]; then as_root=1; shift; fi
keep=" PATH "
while [[ $# -gt 0 && $1 != -- ]]; do keep+="$1 "; shift; done
[[ ${1-} == -- ]] || { echo "mcp-isolate: brak separatora --" >&2; exit 2; }
shift
for v in $(compgen -e); do
[[ $keep == *" $v "* ]] || unset "$v"
done
export LANG=C.UTF-8
if (( as_root )); then
export HOME=/root
exec "$@"
fi
export HOME=/home/mcp
exec setpriv --reuid=65534 --regid=65534 --clear-groups --inh-caps=-all --bounding-set=-all --no-new-privs -- "$@"
Wrapper zostawia w środowisku tylko PATH, LANG, HOME i zmienne wymienione przed --, po czym przechodzi na użytkownika nobody bez capabilities. Katalog settings/ ma uprawnienia 700, więc serwer nie przeczyta konfiguracji huba ani tokenów OAuth. Flaga --root zostawia roota (z czystym środowiskiem) dla serwerów, które muszą zapisywać do /app/artefacts: imagegen i elevenlabs. To świadomy kompromis: te dwa serwery nadal mogą przeczytać /proc/1/environ huba i katalog settings/. Kluczy klientów i tak tam nie znajdą, bo plik zawiera tylko ich skróty, ale sekrety z .env i tokeny OAuth są w ich zasięgu.
Konfiguracja serwerów
Kilka przykładów konfiguracji serwerów MCP:
"tavily": {
"command": "mcp-isolate",
"args": ["TAVILY_API_KEY", "--", "npx", "-y", "tavily-mcp@0.2.22"],
"env": { "TAVILY_API_KEY": "${TAVILY_API_KEY}" },
"enabled": true
}
"google-tasks": {
"command": "mcp-isolate",
"args": [
"GOOGLE_OAUTH_CLIENT_ID", "GOOGLE_OAUTH_CLIENT_SECRET", "USER_GOOGLE_EMAIL", "--", "uvx", "workspace-mcp==2.3.0", "--single-user", "--tools", "tasks"
],
"env": {
"GOOGLE_OAUTH_CLIENT_ID": "${GOOGLE_OAUTH_CLIENT_ID}",
"GOOGLE_OAUTH_CLIENT_SECRET": "${GOOGLE_OAUTH_CLIENT_SECRET}",
"USER_GOOGLE_EMAIL": "robertolechowski@gmail.com"
},
"enabled": true,
"tools": { "google-tasks-start_google_auth": { "enabled": false } }
}
Generator obrazów nie łączy się bezpośrednio z OpenAI, tylko z własną bramką LiteLLM (klucz wirtualny z budżetem):
"imagegen": {
"command": "mcp-isolate",
"args": ["--root", "OPENAI_API_KEY", "OPENAI_BASE_URL", "DEFAULT_OUTPUT_DIR", "--", "npx", "-y", "@napolab/gpt-image-mcp@2.0.3"],
"env": {
"OPENAI_API_KEY": "${LITELLM_MCPHUB_KEY}",
"OPENAI_BASE_URL": "http://litellm:4000/v1",
"DEFAULT_OUTPUT_DIR": "/app/artefacts/imagegen"
},
"enabled": true,
"tools": { "imagegen-batch-edit": { "enabled": false } }
}
Serwery zdalne: usługi w lokalnych kontenerach z wbudowanym endpointem MCP i context7 w chmurze:
"crawl4ai": {
"type": "sse",
"url": "http://crawl4ai:11235/mcp/sse",
"headers": { "Authorization": "Bearer ${CRAWL4AI_API_TOKEN}" },
"enabled": true,
"owner": "admin",
"tools": {
"crawl4ai-ask": { "enabled": false },
"crawl4ai-pdf": { "enabled": false },
"crawl4ai-execute_js": { "enabled": false }
}
},
"context7": {
"type": "streamable-http",
"url": "https://mcp.context7.com/mcp",
"headers": { "CONTEXT7_API_KEY": "${CONTEXT7_API_KEY}" },
"enabled": true
},
"graphiti": {
"type": "streamable-http",
"url": "http://graphiti:8000/mcp",
"enabled": true,
"owner": "admin"
}
Podłączone serwery
| Serwer | Transport | Do czego |
|---|---|---|
tavily | stdio (npx) | wyszukiwanie w sieci, ekstrakcja treści znalezionych stron, research |
brave-search | stdio (npx) | wyszukiwanie web, newsy, miejsca, obrazy, filmy |
crawl4ai | SSE | strona jako Markdown (md), zrzut ekranu, crawl |
firecrawl | stdio (npx) | strony z JS lub blokujące boty, mapa serwisu |
markitdown | stdio (uvx) | PDF, DOCX, XLSX, PPTX i strony do Markdownu |
context7 | streamable-http | aktualna dokumentacja bibliotek |
graphiti | streamable-http | wspólna pamięć agentów (graf wiedzy) |
google-tasks | stdio (uvx) | odczyt i zapis Google Tasks |
imagegen | stdio (npx) | generowanie i edycja obrazów (gpt-image-2) |
elevenlabs | stdio (uvx) | tekst na mowę, mowa na tekst |
Grupy serwerów
Grupa to nazwany zestaw narzędzi do konkretnego zastosowania. Pozwala ograniczyć liczbę tokenów, które agent w każdej sesji zużywa na definicje narzędzi. Klient łączy się z adresem /mcp/<grupa> i widzi tylko narzędzia tej grupy.

Domyślna grupa Claude Code, używana we wszystkich projektach, ma po jednym narzędziu na zadanie:
{
"name": "claude_code",
"description": "Claude Code - domyslna grupa dla wszystkich projektow: po jednym narzedziu na zadanie (wyszukiwanie, tresc strony, dokumenty, dokumentacja bibliotek, pamiec)",
"instructions": "Po jednym narzedziu na zadanie. Wyszukiwanie: tavily_search (tresc znalezionej strony: tavily_extract). Tresc strony: crawl4ai md (darmowy); firecrawl_scrape tylko gdy strona wymaga JS lub blokuje boty (kredyty Firecrawl). PDF/DOCX: markitdown convert_to_markdown. Dokumentacja bibliotek: context7. Pamiec wspolna: graphiti search_memory_facts / add_memory. WAZNE: jesli wiesz, ze do zadania przydalby sie serwer MCP, ktory istnieje, ale nie masz go w tej sesji (...), powiedz to uzytkownikowi wyraznie i na poczatku odpowiedzi: ktory serwer, w jakiej grupie i co by dal - zamiast po cichu obchodzic brak.",
"servers": [
{ "name": "tavily", "tools": ["tavily_search", "tavily_extract"] },
{ "name": "crawl4ai", "tools": ["md"] },
{ "name": "firecrawl", "tools": ["firecrawl_scrape"] },
{ "name": "markitdown", "tools": "all" },
{ "name": "context7", "tools": "all" },
{ "name": "graphiti", "tools": ["search_memory_facts", "add_memory"] }
]
}
Pole instructions hub wysyła klientowi przy initialize, a Claude Code dokłada je do kontekstu sesji jako instrukcje serwera. Dzięki temu kolejność narzędzi (najpierw darmowy crawl4ai, dopiero potem płatny Firecrawl) jest zapisana w jednym miejscu, a nie w CLAUDE.md każdego projektu.
MCPHub nie pozwala zagnieżdżać grup. Dlatego grupy Claude Code są przyrostowe: projekt łączy się z domyślną grupą claude_code i dokłada drugą, która zawiera tylko narzędzia spoza claude_code. Zmiana w grupie bazowej nie wymaga poprawek w pozostałych.
| Grupa | Narzędzia | Klient | Zawartość |
|---|---|---|---|
claude_code | 9 | Claude Code, wszystkie projekty | tavily_search, tavily_extract, crawl4ai md, firecrawl_scrape, markitdown, context7, graphiti |
blog | 17 | Claude Code, repo bloga | imagegen, ElevenLabs, brave_image_search, crawl4ai screenshot, firecrawl_map, Google Tasks |
personal_assistant | 30 | Claude Code, projekt asystenta osobistego | Brave (web, news, miejsca, obrazy, filmy), tavily_research, szerszy Firecrawl, imagegen, Google Tasks, szerszy graphiti |
infra | ok. 50 | Claude Code, repo infrastruktury | odczyt DNS (OVH) i Grafany |
infra_admin | ok. 300 | Claude Code, subagent infra-admin | UniFi (odczyt i zapis), zapis DNS (OVH) i Grafany, skrypty Windmilla, context7, crawl4ai md, firecrawl_scrape |
bambu | 27 | Claude Code, repo infrastruktury | drukarka 3D Bambu Lab: stan, AMS, kamera, sterowanie i wydruk |
szukaj | 3 | Open WebUI, zwykły czat | brave_web_search, tavily_search, crawl4ai md |
research | 6 | Open WebUI | po jednym narzędziu na funkcję: szukanie, strona, badanie |
media | 8 | Open WebUI | imagegen, ElevenLabs |
pamiec | 6 | Open WebUI, domyślnie w każdym czacie | graphiti |
Konfiguracja klienta
Grupę wybiera się adresem URL. Domyślną grupę ustawiam globalnie w ~/.claude.json:
"mcpServers": {
"mcphub": {
"type": "http",
"url": "http://mcphub.mini-1.lan/mcp/claude_code",
"headers": { "Authorization": "Bearer mcphub_***" }
}
}
W pliku .mcp.json w katalogu głównym repozytorium bloga definiuję grupę dla agentów tego projektu:
{
"mcpServers": {
"mcphub-blog": {
"type": "http",
"url": "http://mcphub.mini-1.lan/mcp/blog",
"headers": {
"Authorization": "Bearer ${MCPHUB_CLAUDE_KEY}"
}
}
}
}
Serwer z .mcp.json trzeba jeszcze zatwierdzić w .claude/settings.json projektu:
{
"enabledMcpjsonServers": ["mcphub-blog"]
}
W sesji narzędzia mają prefiks z nazwą wpisu: mcp__mcphub__tavily-tavily_search z grupy bazowej i mcp__mcphub-blog__google-tasks-list_tasks z grupy projektu. Po nazwie widać więc, z której grupy pochodzi narzędzie, co ułatwia reguły uprawnień w settings.json.
Uprawnienia klientów
Każdy klient ma własny klucz bearer z listą dozwolonych grup:
"bearerKeys": [
{ "name": "claude-code", "accessType": "groups",
"allowedGroups": ["claude_code", "infra", "personal_assistant", "media", "infra_admin", "bambu", "blog"],
"tokenPrefix": "mcphub_***", "tokenHash": "***" },
{ "name": "open-webui", "accessType": "groups",
"allowedGroups": ["research", "media", "szukaj", "pamiec"],
"tokenPrefix": "mcphub_***", "tokenHash": "***" },
{ "name": "windmill", "accessType": "groups",
"allowedGroups": ["research"],
"tokenPrefix": "mcphub_***", "tokenHash": "***" },
{ "name": "admin-cli", "accessType": "all",
"tokenPrefix": "mcphub_***", "tokenHash": "***" }
]
Przykłady użycia
Google Tasks z poziomu Claude Code
Lista zadań bloga jest prowadzona w Google Tasks, więc wystarczy prompt:
Pokaż otwarte zadania z listy Blog.
Claude Code wywołuje google-tasks-list_task_lists (identyfikator listy), a potem google-tasks-list_tasks z show_completed: false. Fragment prawdziwej odpowiedzi narzędzia:
Tasks in list TU8ycS1MNW41ZG5nTkY1Mg for robertolechowski@gmail.com:
- Post na blogu: kolejne wersje Pythona — nowa składnia i biblioteki
Status: needsAction
- Post na blogu: opis kontenera mcpHub
Status: needsAction
Notes: Napisać post o własnym kontenerze mcpHub (MCPHub) — centralnym hubie serwerów MCP dla Claude Code i ...
- Post na blogu: role w konfiguracji Claude Code — reguły na konkretny plik
Status: needsAction
- Seria wpisów na blogu: domowy stack multimedialny (*arr + Emby)
Status: needsAction
* Cz. 1: Przegląd całego stacku — jakie elementy, jak współpracują, Docker
* Cz. 2: Radarr i Sonarr — automatyczne pobieranie filmów i seriali
...
Research z wyszukiwarką i pobraniem strony
Znajdź changelog MCPHub dla wersji 1.1.x i wypisz zmiany dotyczące grup i kluczy.
Agent używa tavily_search, a treść znalezionej strony pobiera narzędziem crawl4ai md, które działa w kontenerze na lokalnym serwerze. Firecrawl wchodzi do gry tylko wtedy, gdy strona wymaga JavaScriptu albo blokuje boty. O tej kolejności decyduje instrukcja grupy, nie prompt.
Dokumentacja bibliotek i dokumenty
Jak w markdown-it napisać plugin dla własnego bloku ::: ? Użyj aktualnej dokumentacji.
context7 zwraca fragmenty aktualnej dokumentacji biblioteki zamiast wiedzy z danych treningowych modelu. markitdown zamienia PDF albo DOCX na Markdown, który agent może cytować.
Grafiki i audio do wpisów
Grupa blog ma imagegen i ElevenLabs. Obraz zapisuje się na udziale NAS, a agent kopiuje go do katalogu posta. Oba serwery są płatne, więc instrukcja grupy pozwala z nich korzystać tylko na wyraźne polecenie, a ElevenLabs ma przed dłuższym tekstem sprawdzić limit (check_subscription).
Problemy napotkane przy konfiguracji
- Panel nadpisuje cały plik. Każda zmiana w panelu zapisuje cały
mcp_settings.json, więc równoczesna edycja pliku i panelu gubi jedną ze zmian. - Zapis bez wyłączonych serwerów. Wersja 1.1.10 przy zmianie grup przez API zapisała plik bez trzech wyłączonych serwerów.
- Skrypty czytające klucz z konfiguracji. Mój
hub.pybrał kluczadmin-cliprosto zmcp_settings.json. Po aktualizacji do 1.2.2 poletokenzniknęło, więc skrypt trzeba było przestawić na osobny plik z kluczem. - Ochrona SSRF. Hub blokuje serwery zdalne pod adresami prywatnymi, a takie mają kontenery w sieci Dockera. Wyjątkiem są serwery z
"owner": "admin". - Google Tasks w kontenerze. Token OAuth powstaje jednorazowo na stacji roboczej, a plik z refresh tokenem musi być zapisywalny dla użytkownika
nobody. - Odświeżanie narzędzi w kliencie. U mnie Claude Code nie zawsze reaguje na
tools/list_changed; nowe narzędzia pojawiają się czasem dopiero po reconnect w/mcp.
Claude Code ładuje narzędzia MCP jako deferred: w kontekście sesji są tylko nazwy, a pełny schemat trafia do kontekstu dopiero po ToolSearch. Dla Claude Code liczba narzędzi w grupie ma więc mały wpływ na kontekst. Inaczej jest w Open WebUI, gdzie każda podpięta grupa wysyła pełne schematy przy każdym zapytaniu, dlatego grupy czatowe mają po 3–8 narzędzi.
Podsumowanie
MCPHub zamienia kilkanaście osobno konfigurowanych serwerów MCP w jedną usługę: konfiguracja w gicie, sekrety w .env, klucz i zestaw narzędzi dopasowany do klienta. Najwięcej pracy wymagało nie samo uruchomienie kontenera, tylko bezpieczeństwo (izolacja procesów stdio, klucze ograniczone do grup, wyłączone narzędzie logowania OAuth w Google Tasks) i pilnowanie, żeby panel nie nadpisał konfiguracji. Raz ustawiony hub sprawia, że dodanie nowego narzędzia dla wszystkich klientów to jeden wpis w pliku i jedna linia w grupie.




