MCP Hub
title: Pioneer emoji: 🔥 colorFrom: purple colorTo: pink sdk: docker app_port: 7860 pinned: false license: mit
MCP Hub
Ein HF Space mit mehreren MCP-Servern, getrennt über Pfade, jeweils mit eigenem Auth-Key.
Nach dem Plugin-Prinzip von local-mcp-hub neu aufgebaut: Ein MCP = ein py, main.py erkennt und montiert automatisch, neue MCPs hinzufügen ohne die Hauptdatei zu ändern.
Related MCP server: MCP Hub
Struktur
hub-mcp/
├── main.py ← 插件自动发现 + 鉴权壳 + 路由装配
├── Dockerfile ← ⚠️ GitHub 侧完整构建定义,与 HF 侧那份内容不同,见「构建部署链路」
├── requirements.txt
├── .github/workflows/build.yml ← GHCR 镜像构建(含防套娃闸门)
├── duck-mcp/ ← duck-mcp TS 原版完整项目(npm install + tsc build 出 dist/)
└── mcps/
├── _ddg.py ← 库:DDG 搜索/抓取实现(下划线开头,不加载为插件)
├── _stdio_bridge.py ← 库:stdio 子进程桥公共实现(duck / academic 共用,见「踩坑档案 #1」)
├── doubao-mcp.py → /doubao/sse web_search
├── zhihu-mcp.py → /zhihu/sse zhihu_search / global_search / zhihu_ask / zhihu_trending
├── ddg-mcp.py → /ddg/sse search / scrape(旧版,已被 /duck 取代)
│ + REST: POST /ddg/search、/ddg/scrape(给 rikkahub 安卓端)
├── duck-mcp.py → /duck/sse 桥:bash -c 'cd duck-mcp && node dist/index.js'
└── academic-mcp.py → /academic/sse 桥:/opt/academic-venv/bin/academic-mcpSubprozess-Brücke (duck / academic)
Diese beiden sind nicht selbst implementiert, sondern starten den jeweiligen Original-MCP-Server als Subprozess und kommunizieren per stdio mit ihm.
Der Hub macht nur Protokoll-Forwarding (tools/list, tools/call werden unverändert durchgereicht):
duck: Upstream ist ein TS-Projekt (VM-Sandbox löst Anti-Bot-Challenge + Chrome134 TLS-Fingerprint). Eine Portierung nach Python wäre zu teuer, das ganze Projekt liegt in
duck-mcp/, im Image läuft es mit node 22 alsdist/index.js.academic: Reines Python, aber die Abhängigkeiten (fastmcp) kollidieren mit dem
mcp==1.2.0des Hubs, daher in einem separaten venv/opt/academic-venvisoliert installiert.
Die gemeinsame Implementierung liegt in mcps/_stdio_bridge.py, jeder Aufruf startet eine eigene Session, die danach sofort geschlossen wird –
das ist keine Faulheit, sondern von anyio erzwungen, siehe „Stolperfallen-Archiv #1", füg ja keinen Session-Cache hinzu.
Endpunkte
MCP | SSE-Endpunkt | Tools |
Doubao-Suche |
|
|
Zhihu |
|
|
DuckDuckGo |
|
|
DuckDuckGo (Original-TS-Brücke) |
|
|
Akademische Paper |
|
|
Endpunkt-Status getestet (2026-08-20)
Endpunkt | tools/list | Tatsächlicher Aufruf | Hinweis |
| ✅ | ✅ | Kostenloses Kontingent Custom+Global gemeinsam 500 Aufrufe/Monat, nicht aufbrauchen |
| ✅ | ✅ | |
| ✅ 3 Tools | ✅ echte Paper zurück | arXiv funktioniert, Quellen ohne Key (Scopus/WOS/CORE/IEEE…) nur Warnung, kein Problem |
| ✅ 9 Tools | ⚠️ Brücke ok, Upstream blockiert | DDG liefert Anti-Bot-Challenge für HF-Rechenzentrums-IPs, kein Code-Problem, IP wechseln / Proxy nutzen |
| ✅ | ⚠️ | Alte Version, stirbt leichter am Anti-Scraping, für REST behalten, kann entfernt werden |
Die Parameter-Falle bei academic
paper_search / paper_download erwarten ein query_list-Objekt-Array, keinen String:
{"query_list": [{"query": "quantum computing", "searcher": "arxiv", "max_results": 2}]}searcher weglassen = alle Quellen durchsuchen (langsam). paper_read erwartet dagegen {"searcher": ..., "paper_id": ...}.
REST-Endpunkte (für rikkahub Android-Client, kein MCP)
Methode | Pfad | body | Rückgabe |
POST |
|
|
|
POST |
|
|
|
Die Rückgabe ist strukturell identisch mit SearchResult / ScrapedResult von rikkahub, der Client kann direkt deserialisieren.
Auth ebenfalls Authorization: Bearer <DDG_KEY>, bei Fehlern kommt {"detail": "..."} zurück.
Authentifizierung
Jeder MCP hat einen eigenen Bearer-Key (Authorization: Bearer <key>):
MCP | Key-Env | Standardwert |
doubao |
|
|
zhihu |
|
|
ddg |
|
|
duck |
|
|
academic |
|
|
Wenn die Env-Variable gesetzt ist, wird ihr Wert verwendet, sonst der Standard. Auf der GET /-Startseite sieht man, ob die Auth für jeden Endpunkt konfiguriert ist und ob die Upstream-Secrets vorhanden sind.
Upstream-Secrets (in HF Space Settings → Secrets eintragen, nicht ins Repo schreiben)
env | Zweck |
| Volcano Ark Doubao-Suche Custom-Version API-Key (Pflicht, Global-Version fällt darauf zurück wenn nicht gesetzt) |
| Dedizierter Key für die Doubao-Suche Global-Version (optional, unter „API-Key-Verwaltung – nutzungsabhängig bezahlen" erstellen, ohne diesen nutzt die Global-Version den ARK-Key und meldet wahrscheinlich 700901) |
| Zhihu Open Platform Access Secret |
Neues MCP hinzufügen
Ein py in mcps/ ablegen, main.py muss nicht geändert werden:
"""第一行 docstring 会显示在 / 首页 about 里。"""
import os
from mcp import types
from mcp.server import Server
MOUNT = "myname" # 可选,默认用文件名(去掉 .py)
KEY_ENV = "MY_KEY" # 可选,Bearer 鉴权 env 名
DEFAULT_KEY = "" # 可选,默认 key(env 没配时用)
# ENABLED = False # 可选,临时停用
server = Server("My Server")
@server.list_tools()
async def list_tools() -> list[types.Tool]:
...
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
...Nicht
if __name__ == "__main__": server.run(...)schreiben – Port und Routen verwaltet der Hub.Ein py mit mehreren Endpunkten:
MOUNTS = {"path1": srv1, "path2": srv2}.Zusätzliche REST-Routen (Einzel-Mount):
ROUTES = [starlette.Route("/xxx", endpoint=..., methods=["POST"])], wird unter dem eigenen Plugin-Pfad gemountet.Bibliotheksdateien im selben Verzeichnis referenzieren:
import _xxx(Dateien mit Unterstrich am Anfang werden nicht als Plugin geladen).Wenn ein einzelnes Plugin beim Import scheitert, erscheint nur ein Fehler unter
brokenauf/, andere Plugins sind nicht betroffen.
Lokal ausführen
pip install -r requirements.txt
uvicorn main:app --port 7860Build- und Deployment-Kette
Die Build-Umgebung von HF Space hat viele Einschränkungen (bun lässt sich nicht installieren, nicht mal curl gibt es), daher wird nicht in HF gebaut:
改代码 → push GitHub(fuwei99/hub-mcp) → Actions 构建镜像 → 推 GHCR
↓
HF 的 Dockerfile 只 FROM 拉现成镜像Die beiden Dockerfiles haben unterschiedlichen Inhalt, jedes macht genau eine Sache:
Ort | Inhalt | Zweck |
GitHub |
| Baut das echte Image |
HF |
| Zieht nur das fertige Image und führt es aus |
🚨 Eiserne Regeln
1. Das HF-Dockerfile darf auf keinen Fall zurück nach GitHub synchronisiert werden. Sonst macht Actions „Matroschka-Build": Das Image der letzten Version wird unverändert weitergereicht und neu gepusht, kein einziger
COPY-Schritt wird ausgeführt, das Image ist immer alter Code, aber der Build zeigt success. Schon zweimal reingefallen (siehe Stolperfallen-Archiv #2). Konkrete Mine: Wenn das lokale Repo als Remote auf HF zeigt, auf keinen Fallgit checkout origin/main -- Dockerfileauf dem Dockerfile ausführen, das zieht die HF-Version ins Lokale und pusht sie mit nach GitHub.2. Auf HF-Seite den Digest pinnen, nicht
:latestverwenden. HF-Build cached den alten Digest von latest, wenn das Tag sich nicht ändert, werden die Layer nicht neu gezogen → Code geändert, online ist trotzdem die alte Version.3. Vor dem Deployment prüfen, nicht blind „success" glauben. Die Code-Layer aus GHCR herausholen und prüfen, ob die Dateien stimmen (Methode unten), das ist schneller als sich in den Online-Logs die Stirn zu stoßen.
Standard-Ablauf für Image-Wechsel
# 1. 改代码,只推 GitHub(注意:Dockerfile 必须是完整构建版)
git push --force https://github.com/fuwei99/hub-mcp.git main:main
# 2. 等 Actions(workflow 已带防呆闸门,套娃/缺 COPY 会直接 fail)
curl -H "Authorization: Bearer $GITHUB_TOKEN_FUWEI" \
"https://api.github.com/repos/fuwei99/hub-mcp/actions/runs?per_page=1"
# 3. 取新 digest
tok=$(curl -s "https://ghcr.io/token?scope=repository:fuwei99/hub-mcp:pull" | jq -r .token)
curl -sI -H "Authorization: Bearer $tok" \
-H "Accept: application/vnd.oci.image.index.v1+json" \
"https://ghcr.io/v2/fuwei99/hub-mcp/manifests/latest" | grep -i docker-content-digest
# 4. 改 HF 的 Dockerfile FROM 行为该 digest,推 HF
# 5. 验证线上真的换了代码(找个只有新版才有的字符串)
curl -s https://fluidgender159-hub-mcp.hf.space/ | jq .aboutPrüfen: Dateien aus GHCR holen
Ohne docker, mit reinem curl lassen sich die Image-Layer aufziehen (ultimatives Mittel um zu prüfen, ob der Build wirklich greift):
tok=$(curl -s "https://ghcr.io/token?scope=repository:fuwei99/hub-mcp:pull" | jq -r .token)
A="Accept: application/vnd.oci.image.index.v1+json, application/vnd.oci.image.manifest.v1+json"
# index → amd64 manifest → 找几 KB 的小层(就是 COPY mcps/ 那层)→ 拉 blob 解 tar
curl -s -H "Authorization: Bearer $tok" -H "$A" \
"https://ghcr.io/v2/fuwei99/hub-mcp/manifests/latest" -o idx.json
# ...取 amd64 digest、取 layers 里 size < 20000 的、curl blobs/<digest> | tar tzAuch in den Actions-Logs sieht man die Matroschka sofort: Ein normaler Build hat COPY und dauert 1–2 Minuten;
ein Matroschka-Build hat nur resolve ghcr.io/... done + exporting layers, in 2 Sekunden fertig.
Stolperfallen-Archiv
#1 ⭐ anyio cancel scope kann Tasks nicht überbrücken (wahre Ursache für hängende Subprozess-Brücke)
Symptom: /duck/sse, /academic/sse verbinden sich, initialize antwortet sofort, aber tools/list
bleibt für immer still – kein Fehler, kein Timeout, im SSE nur pings. Jeder MCP-Client hängt „fest".
Falsch verdächtigte Richtungen (alle nicht die Ursache): SSE-Long-Connection, Testskript, node startet nicht, Banner verschmutzt stdout (Banner geht über stderr, stdout ist sauber).
Wahre Ursache: stdio_client() und ClientSession() sind beide task-gebundene anyio-Kontexte.
Die Brücke hatte zur Ersparnis in der Vergangenheit im Task von Anfrage A __aenter__ aufgerufen und die Session global gecacht, um sie für Anfrage B wiederzuverwenden.
Im Hub ist aber jede SSE-Verbindung ein eigener Task, also:
RuntimeError: Attempted to exit cancel scope in a different task than it was entered inDas Verhalten ist heimtückisch: initialize antwortet, weil das die Brückenschale selbst beantwortet, ohne den Subprozess anzufassen;
sobald tools/list echte Subprozess-Weiterleitung braucht, stirbt es am task-übergreifenden cancel scope.
Reproduktion (30-Zeilen-Lokalskript, kein Deployment nötig):
async def task_a():
cm = stdio_client(params); read, write = await cm.__aenter__()
scm = ClientSession(read, write); s = await scm.__aenter__()
await s.initialize(); state["s"] = s # 缓存给别的 task
async def task_b():
await state["s"].list_tools() # 💥 死这儿
await asyncio.create_task(task_a())
await asyncio.create_task(task_b())Fix: mcps/_stdio_bridge.py – bei jedem list_tools/call_tool wird im aktuellen Task
ein Subprozess gestartet, mit async with geschlossen, nach Gebrauch sofort zu; nur die Tool-Beschreibungen werden gecacht (reine Daten, task-übergreifend ok).
async with stdio_client(self._params_factory()) as (read, write):
async with ClientSession(read, write) as session:
await asyncio.wait_for(session.initialize(), timeout=self._timeout)
return await asyncio.wait_for(fn(session), timeout=self._timeout)Nicht „optimieren" zu einer geteilten Session. Für echte Beschleunigung wäre der richtige Weg ein dedizierter langlaufender Worker-Task + Queue, alle IO passieren in diesem Task, statt Kontextobjekte über Tasks hinweg zu reichen.
Zusätzliche Lehre: ClientSession(read, write) nur zu instanziieren ohne __aenter__ hängt ebenfalls –
der Hintergrund-Task „stdout lesen → Antworten verteilen" startet erst in __aenter__,
ohne Kontexteinstieg bekommt keine gesendete Anfrage eine Antwort.
#2 ⭐ Matroschka-Build (Image immer alter Code, aber Build zeigt success)
Symptom: Code geändert, Actions success, HF rebuild RUNNING, aber online ändert sich nichts. Wiederholt HF-Cache, GHCR-Cache, Layer-Cache verdächtigt, alles nicht.
Lokalisierungsmethode: Aus GHCR den COPY mcps/-Layer herausgeholt und tar tzf – die neu hinzugefügte
_stdio_bridge.py ist gar nicht im Image, obwohl sie auf GitHub existiert. Dann die Actions-Logs angesehen:
#1 transferring dockerfile: 647B ← 完整版有 2.7KB
#5 resolve ghcr.io/fuwei99/hub-mcp@sha256:0799864b... done
#7 exporting layers done ← 全程 2 秒,零 COPYWahre Ursache: Das Dockerfile im GitHub-Repo war zur HF-Version geworden –
FROM ghcr.io/fuwei99/hub-mcp@sha256:... – Actions hat das alte Image unverändert weitergereicht und neu gepusht.
Wie es reinkam: Das lokale Repo hatte HF als Remote, und
git checkout origin/main -- Dockerfile hat die HF-Version in den Arbeitsbereich gezogen,
die dann beim Push nach GitHub mitging.
Vorsorge (in .github/workflows/build.yml eingebaut, bei Wiederholung schlägt der Build sofort fehl):
- name: 拒绝套娃构建
run: |
if grep -qE '^FROM +ghcr\.io/fuwei99/hub-mcp' Dockerfile; then
echo "::error::Dockerfile 是 HF 版,会套娃构建"; exit 1
fi
grep -q 'COPY mcps/' Dockerfile || { echo "::error::缺少 COPY mcps/"; exit 1; }Zusätzlich hat das Dockerfile einen Build-Zeit-Selbsttest: test -f mcps/_stdio_bridge.py || exit 1.
#3 Der Abhängigkeits-Albtraum von academic-mcp
Upstream academic-mcp==0.1.7 hat keine Obergrenzen für Abhängigkeiten, die installierte Kombination ist kaputt. Dreifach-Fehler:
Fehler | Ursache |
| fastmcp braucht es, aber academic-mcp deklariert es nicht |
| Es wurde |
| Zweistufiges |
Fix: Alles in einem Befehl installieren, Obergrenzen explizit setzen, und einen Import-Selbsttest in die Build-Phase legen:
RUN python3 -m venv /opt/academic-venv \
&& /opt/academic-venv/bin/pip install --no-cache-dir \
academic-mcp==0.1.7 pydantic-settings "mcp<2.0" \
&& /opt/academic-venv/bin/python -c "from fastmcp import FastMCP; \
from academic_mcp.__main__ import main; print('academic-mcp import OK')"Getestete funktionierende Kombination: academic-mcp 0.1.7 + fastmcp 3.4.7 (oder 2.14.1) + mcp 1.29.0
pydantic-settings 2.15.0.
Lehre: Beim Upgrade von Abhängigkeiten nicht zweistufig pip install und dann pip install -U machen, alles in einem Rutsch installieren, damit der Resolver einheitlich entscheidet;
Abhängigkeitskombinationen unbedingt lokal im venv testen, bevor sie ins Dockerfile kommen, und den Import-Selbsttest in die Build-Phase legen –
falsch installiert = Build schlägt fehl, statt auf Fehler in den Online-Logs zu warten.
#4 Sonstiges
Symptom | Ursache | Fix |
bun-Download exit 127 |
| Erst |
bun | HF-Build-Umgebungsbeschränkung | Node 22 offizielles Tarball verwenden, mit |
|
| Einzelnen Parameter nach alter Signatur übergeben |
academic meldet |
| Reine lokale Umgebungsbeschränkung, HF/docker normal. Download-Verzeichnis separat setzen: |
Lokal/HF-Remote divergieren | Paralleles Pushen auf beiden Seiten | Nach rebase force-pushen; oder eine saubere Clone nur fürs Deployment anlegen |
Fehlersuche-Methodik (der zeitsparende Teil)
Nicht den Python-MCP-Client zum Debuggen von Hängern verwenden – der hängt selbst, man sieht nicht wo. Mit curl das Protokoll von Hand durchspielen, Frame für Frame sehen wer nicht antwortet:
curl -sN -H "Authorization: Bearer wei123.." "$BASE/duck/sse" > sse.log & SID=$(grep -o 'session_id=[a-f0-9]*' sse.log | head -1 | cut -d= -f2) P="$BASE/duck/messages/?session_id=$SID" curl -X POST "$P" -d '{"jsonrpc":"2.0","id":1,"method":"initialize",...}' curl -X POST "$P" -d '{"jsonrpc":"2.0","method":"notifications/initialized"}' curl -X POST "$P" -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' # 盯 sse.log:initialize 回了但 id:2 不回 → 问题在桥拉子进程那一步Schichtweise lokalisieren: Brückenschale → kann der Subprozess allein laufen → Subprozess-Bibliotheksfunktion direkt aufrufen. In diesem Fall funktionierte
ArxivSearcher().search()direkt aufgerufen einwandfrei, die Suche selbst ist also ok, der Fehler liegt in der Verpackungsschicht.In den Online-Logs gegen Wände zu laufen ist am teuersten. Was sich lokal mit dem Hub reproduzieren lässt, niemals online testen; Abhängigkeitsprobleme in die Build-Phase als Selbsttest legen, damit es schon in Actions explodiert.
serverInfo.versionist nicht die Code-Version (das ist die mcp-Bibliotheksversion). Um zu prüfen, ob online neuer Code läuft, nach einem String suchen, den es nur in der neuen Version gibt, z. B. den about-Text vonGET /.
This server cannot be deployed
Maintenance
Related MCP Connectors
MCP server for building and testing AI agents with multi-model experimentation and insights.
Hosted MCP server for LLM cost estimation, model comparison, and budget-aware routing.
MCP server connecting AI agents to 100+ apps (Gmail, Slack, Notion, GitHub) via one-click OAuth.
Related MCP Servers
- AlicenseNot gradedqualityAmaintenanceAn MCP server that provides Hugging Face Hub API and Search endpoints through multiple transport protocols (STDIO, SSE, StreamableHTTP, and StreamableHTTPJson), enabling integration with AI model capabilities.302MIT
- FlicenseNot gradedqualityNot gradedmaintenanceMCP Hub aggregates and proxies multiple Model Context Protocol servers into a unified Streamable HTTP interface. It allows users to combine diverse stdio, SSE, and HTTP-based servers while providing tool namespacing, health monitoring, and secure authentication.474 npm-
- AlicenseNot gradedqualityCmaintenanceZero-auth multi-source research MCP server that enables web search, reading URLs, PDFs, GitHub repos, and querying Hacker News, Stack Overflow, Semantic Scholar, and YouTube transcripts without API keys.11Apache 2.0
- FlicenseNot gradedqualityBmaintenanceAn extensible MCP hub that exposes internal services (chat, observability, RAG) as namespaced tools via FastMCP, with OpenAPI auto-generation, auth, and resilient error handling.-