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ć.

Koszty konfiguracji serwerów MCP „per klient”
KosztNa czym polega
Klucze API w wielu miejscachTen 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 roboczejKaż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 zakresemAgent 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 kontekstuKlienci 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:

Funkcje MCPHub
FunkcjaCo robi hub
Serwery stdiouruchamia 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ówwystawia narzędzia przez Streamable HTTP pod adresem /mcp/<grupa>
Zarządzanie narzędziamiwłącza i wyłącza pojedyncze narzędzia, nadpisuje ich opisy i ukrywa parametry
Autoryzacja klientówklucze bearer ograniczone do wybranych grup, w konfiguracji zapisane tylko jako skrót SHA-256
API RESTwszystko, 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.json jest częścią repozytorium konfiguracji.
  • Cache npx i uvx na hoście. Po odtworzeniu kontenera pakiety nie są pobierane od nowa.
  • Katalog artefacts na NAS. Serwery generujące pliki (obrazy, audio) zapisują wyniki na udziale NAS.
  • Klucze API poza gitem. Sekrety serwerów MCP leżą w pliku .env obok pliku compose, który nie trafia do repozytorium. W mcp_settings.json wystę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

Wybrane serwery MCP w hubie (nie wszystkie)
SerwerTransportDo czego
tavilystdio (npx)wyszukiwanie w sieci, ekstrakcja treści znalezionych stron, research
brave-searchstdio (npx)wyszukiwanie web, newsy, miejsca, obrazy, filmy
crawl4aiSSEstrona jako Markdown (md), zrzut ekranu, crawl
firecrawlstdio (npx)strony z JS lub blokujące boty, mapa serwisu
markitdownstdio (uvx)PDF, DOCX, XLSX, PPTX i strony do Markdownu
context7streamable-httpaktualna dokumentacja bibliotek
graphitistreamable-httpwspólna pamięć agentów (graf wiedzy)
google-tasksstdio (uvx)odczyt i zapis Google Tasks
imagegenstdio (npx)generowanie i edycja obrazów (gpt-image-2)
elevenlabsstdio (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.

Konfiguracja grup narzędzi w panelu MCPHub

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.

Grupy huba i ich klienci
GrupaNarzędziaKlientZawartość
claude_code9Claude Code, wszystkie projektytavily_search, tavily_extract, crawl4ai md, firecrawl_scrape, markitdown, context7, graphiti
blog17Claude Code, repo blogaimagegen, ElevenLabs, brave_image_search, crawl4ai screenshot, firecrawl_map, Google Tasks
personal_assistant30Claude Code, projekt asystenta osobistegoBrave (web, news, miejsca, obrazy, filmy), tavily_research, szerszy Firecrawl, imagegen, Google Tasks, szerszy graphiti
infraok. 50Claude Code, repo infrastrukturyodczyt DNS (OVH) i Grafany
infra_adminok. 300Claude Code, subagent infra-adminUniFi (odczyt i zapis), zapis DNS (OVH) i Grafany, skrypty Windmilla, context7, crawl4ai md, firecrawl_scrape
bambu27Claude Code, repo infrastrukturydrukarka 3D Bambu Lab: stan, AMS, kamera, sterowanie i wydruk
szukaj3Open WebUI, zwykły czatbrave_web_search, tavily_search, crawl4ai md
research6Open WebUIpo jednym narzędziu na funkcję: szukanie, strona, badanie
media8Open WebUIimagegen, ElevenLabs
pamiec6Open WebUI, domyślnie w każdym czaciegraphiti

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.py brał klucz admin-cli prosto z mcp_settings.json. Po aktualizacji do 1.2.2 pole token zniknęł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.