MCP Hub
title: Pioneer emoji: 🔥 colorFrom: purple colorTo: pink sdk: docker app_port: 7860 pinned: false license: mit
MCP Hub
1つのHF Spaceに複数のMCP Serverをマウントし、パスで区別して、それぞれ独立した認証キーを持ちます。
local-mcp-hub のプラグイン方式に合わせて再構築:1つのMCPに1つのpy、main.py が自動で発見・組み立てを行い、新しいMCPを追加してもメインファイルを変更する必要はありません。
Related MCP server: MCP Hub
構造
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-mcp子プロセスブリッジ(duck / academic)
これらは自前実装ではなく、上流のオリジナルMCPサーバーを子プロセスとして起動し、stdioで通信します。
ハブはプロトコル転送のみ行います(tools/list、tools/call をそのまま透過):
duck:上流はTSプロジェクト(VMサンドボックスでanti-botチャレンジを解決 + Chrome134 TLSフィンガープリント)。 Pythonへの移植コストが高すぎるため、プロジェクト全体を
duck-mcp/に格納し、イメージ内でnode 22を使ってdist/index.jsを実行します。academic:純Pythonですが、依存関係(fastmcp)がハブの
mcp==1.2.0と競合するため、独立したvenv/opt/academic-venvに分離してインストールします。
共通実装は mcps/_stdio_bridge.py にあり、呼び出しのたびに独立したセッションを起動し、使い終わったら閉じます。
これは手抜きではなく、anyioに強制された結果です。理由は「ハマりどころアーカイブ #1」を参照。セッションキャッシュを追加しないでください。
エンドポイント
MCP | SSEエンドポイント | ツール |
豆包検索 |
|
|
知乎 |
|
|
DuckDuckGo |
|
|
DuckDuckGo(オリジナルTSブリッジ) |
|
|
学術論文 |
|
|
エンドポイントの実測ステータス(2026-08-20)
エンドポイント | tools/list | 実際の呼び出し | 備考 |
| ✅ | ✅ | 無料枠はCustom+Globalで合計500回/月、使い切らないように注意 |
| ✅ | ✅ | |
| ✅ 3ツール | ✅ 実際の論文を返す | arXivは成功、キー不足のソース(Scopus/WOS/CORE/IEEE…)は警告のみで影響なし |
| ✅ 9ツール | ⚠️ ブリッジは接続、上流がブロック | DDGがHFデータセンターのIPにanti-botチャレンジを返す、コードの問題ではない、IP変更/プロキシが必要 |
| ✅ | ⚠️ | 旧版、反スクレイピングで死にやすい、REST用に残してある、削除を検討してもよい |
academicのパラメータの落とし穴
paper_search / paper_download は query_list オブジェクトの配列を受け取ります。文字列ではありません:
{"query_list": [{"query": "quantum computing", "searcher": "arxiv", "max_results": 2}]}searcher を省略 = 全ソースを検索(遅い)。paper_read は {"searcher": ..., "paper_id": ...} です。
RESTエンドポイント(rikkahub Android用、MCPではない)
メソッド | パス | body | 戻り値 |
POST |
|
|
|
POST |
|
|
|
戻り値はrikkahubの SearchResult / ScrapedResult と完全に同型なので、クライアントはそのままデシリアライズできます。
認証も Authorization: Bearer <DDG_KEY> で、エラー時は {"detail": "..."} を返します。
認証
各MCPは独立したBearerキー(Authorization: Bearer <key>)を持ちます:
MCP | key env | デフォルト値 |
doubao |
|
|
zhihu |
|
|
ddg |
|
|
duck |
|
|
academic |
|
|
envが設定されていればenvの値を使い、なければデフォルトを使います。GET / のホームページで各エンドポイントの認証設定状況や上流のシークレットが配置されているかを確認できます。
上流のシークレット(HF Space Settings → Secrets に置く、リポジトリには書かない)
env | 用途 |
| 火山方舟豆包検索 Custom 版 API キー(必須、Global 版未設定時はこれにフォールバック) |
| 豆包検索 Global 版専用キー(任意、「APIキー管理-従量課金」で作成、未設定だと Global 版は ARK キーを使い、高確率で 700901 エラー) |
| 知乎オープンプラットフォームの Access Secret |
新しいMCPを追加する
mcps/ にpyファイルを置くだけ、main.py の変更は不要:
"""第一行 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]:
...書かない
if __name__ == "__main__": server.run(...)—— ポートとルーティングはハブが管理します。1つのpyで複数のエンドポイントをマウント:
MOUNTS = {"path1": srv1, "path2": srv2}。追加のRESTルートを持たせたい場合(単一マウント):
ROUTES = [starlette.Route("/xxx", endpoint=..., methods=["POST"])]、このプラグインのパスの下にマウントされます。同じディレクトリのライブラリファイルを参照したい場合:
import _xxx(アンダースコアで始まるファイルはプラグインとして読み込まれません)。単一のプラグインのimport失敗は
/のbrokenにエラーが表示されるだけで、他のプラグインには影響しません。
ローカルで実行
pip install -r requirements.txt
uvicorn main:app --port 7860ビルド・デプロイの流れ
HF Space のビルド環境は制限が多い(bun がインストールできない、curl もない)ため、HF 内ではビルドしません:
改代码 → push GitHub(fuwei99/hub-mcp) → Actions 构建镜像 → 推 GHCR
↓
HF 的 Dockerfile 只 FROM 拉现成镜像両側の Dockerfile は内容が異なり、それぞれ役割が違います:
場所 | 内容 | 役割 |
GitHub |
| 実際にイメージをビルド |
HF |
| 既存のイメージを引っ張って実行するだけ |
🚨 鉄の掟
1. HF の Dockerfile を GitHub に同期してはいけない。 そうしないと Actions が「入れ子ビルド」を起こします:前のバージョンのイメージをその場で転用して再プッシュし、
COPYステップが一切実行されず、 イメージは常に古いコードのまま、ビルドは success と表示される。すでに2回失敗しています(ハマりどころ #2 参照)。 具体的な地雷:ローカルリポジトリの remote が HF を指している場合、Dockerfile に対してgit checkout origin/main -- Dockerfileを実行しないでください。HF 版をローカルに取り込んで、一緒に GitHub にプッシュしてしまいます。2. HF 側は digest を固定し、
:latestを使わない。 HF のビルドは latest の古い digest をキャッシュするため、タグが変わらなければレイヤーを再取得しません → コードを変更しても本番は古いまま。3. デプロイ前に検証する。盲目的に "success" を信じない。 GHCR からコードレイヤーを取り出してファイルが正しいか確認する(方法は下記)方が、本番ログで何度も壁にぶつかるより速い。
イメージを変更する標準的な流れ
# 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 .about検証:GHCR からファイルを取り出して確認
docker を使わず、curl だけでイメージレイヤーを開けます(ビルドが実際に有効かどうかを判断する究極の手段):
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 tzActions のログでも入れ子ビルドは一目でわかります:正常なビルドには COPY があり、1〜2分かかります。
入れ子ビルドは resolve ghcr.io/... done + exporting layers だけで、2秒で終わります。
ハマりどころアーカイブ
#1 ⭐ anyio cancel scope はタスクをまたげない(子プロセスブリッジがハングする真の原因)
現象:/duck/sse、/academic/sse には接続でき、initialize は即座に返るが、tools/list
永久に沈黙 —— エラーも出ず、タイムアウトもせず、SSE には ping だけが流れる。どの MCP クライアントでも「ハング」します。
誤って判断した方向(どれも原因ではない):SSE 長接続、テストスクリプト、node が起動しない、 banner が stdout を汚す(banner は stderr に出力されるので、stdout はクリーン)。
真の原因:stdio_client() と ClientSession() はどちらも task-bound の anyio コンテキストです。
ブリッジは当初、オーバーヘッドを減らすために、A リクエストのタスク内で __aenter__ した後、セッションをグローバルにキャッシュして B リクエストで再利用していました。
しかし、ハブでは各 SSE 接続が独立したタスクなので、次のようになります:
RuntimeError: Attempted to exit cancel scope in a different task than it was entered inその挙動は非常に陰険です:initialize が返るのは、ブリッジシェルが自分で答えただけで、子プロセスには触れていないからです。
tools/list で実際の子プロセス転送が必要になると、タスクをまたいだ cancel scope で死にます。
再現(30行のローカルスクリプト、デプロイ不要):
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())修正:mcps/_stdio_bridge.py —— 毎回 list_tools/call_tool を現在のタスク内で
子プロセスを起動し、async with で閉じ、使い終わったら即閉じます。ツールの説明(純データ、タスクをまたげる)だけをキャッシュします。
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)共有セッションに「最適化」しないでください。本当に高速化したいなら、専用の常駐ワーカータスク + キューを起動し、すべてのIOをそのタスク内で行うのが正しい方法です。コンテキストオブジェクトをタスク間で渡すのはやめてください。
ついでの教訓:ClientSession(read, write) を new するだけで __aenter__ しないとハングします ——
バックグラウンドの「stdout を読む → レスポンスをディスパッチ」タスクは __aenter__ 内で起動されるので、
コンテキストに入らなければ、送信したリクエストの応答を受け取る人がいません。
#2 ⭐ 入れ子ビルド(イメージは常に古いコード、ビルドは success と表示)
現象:コードを変更し、Actions が success、HF が RUNNING で再構築されたが、本番の挙動はまったく変わらない。 HF キャッシュ、GHCR キャッシュ、レイヤーキャッシュを疑ったが、どれも違った。
特定手段:GHCR から COPY mcps/ のレイヤーを取り出して tar tzf で確認 —— 新しく追加した
_stdio_bridge.py がイメージに存在しないのに、GitHub にはある。Actions のログを見ると:
#1 transferring dockerfile: 647B ← 完整版有 2.7KB
#5 resolve ghcr.io/fuwei99/hub-mcp@sha256:0799864b... done
#7 exporting layers done ← 全程 2 秒,零 COPY真の原因:GitHub リポジトリの Dockerfile が HF 版の
FROM ghcr.io/fuwei99/hub-mcp@sha256:... になっていた —— Actions が古いイメージをその場で転用して再プッシュしていた。
どうやって混入したか:ローカルリポジトリの remote が HF で、git checkout origin/main -- Dockerfile を実行して HF 版を作業領域に取り込み、その後 GitHub にプッシュする際に一緒に送ってしまった。
防止策(.github/workflows/build.yml に追加済み、再発したらその場でビルド失敗):
- 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; }さらに Dockerfile にもビルド時自己チェックを追加:test -f mcps/_stdio_bridge.py || exit 1。
#3 academic-mcp の依存関係地獄
上流の academic-mcp==0.1.7 は依存関係の上限を固定しておらず、インストールされた組み合わせが壊れています。3連続のエラー:
エラー | 原因 |
| fastmcp が必要だが、academic-mcp が宣言していない |
|
|
| 2段階で |
修正:1つのコマンドでまとめてインストールし、上限を明示的に固定し、ビルド時に import 自己チェックを実行:
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')"実測で使える組み合わせ:academic-mcp 0.1.7 + fastmcp 3.4.7(または 2.14.1)+ mcp 1.29.0
pydantic-settings 2.15.0。
教訓:依存関係のアップグレードは2段階の pip install をせず、1回でまとめてインストールしてパーサーに統一判断させる。
依存関係の組み合わせは必ずローカル venv で実測してから Dockerfile に書き込み、import 自己チェックをビルド時に組み込む ——
インストールが間違っていればビルドが失敗し、本番ログでエラーを待つ必要がない。
#4 その他
現象 | 原因 | 修正 |
bun ダウンロード exit 127 |
| 先に |
bun | HF ビルド環境の制限 | Node 22 公式 tarball に変更、 |
|
| 旧シグネチャに合わせて単一引数を渡す |
academic が |
| 純粋にローカル環境の制限、HF/docker では正常。ダウンロードディレクトリは別途 |
ローカル/HF リモートが分岐 | 両側で並行プッシュ | rebase 後に強制プッシュ;またはデプロイ専用にクリーンな clone を1つ作成 |
トラブルシューティングの方法論(時間を節約する部分)
Python MCP クライアントでハング問題をデバッグしない —— それ自体もハングし、どこで詰まっているかわからない。 curl でプロトコルを手打ちし、フレームごとに誰が応答しないかを確認:
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 不回 → 问题在桥拉子进程那一步レイヤーごとに特定:ブリッジシェル → 子プロセスが単独で実行できるか → 子プロセスのライブラリ関数を直接呼ぶ。 この例では
ArxivSearcher().search()を直接呼ぶと正常で、検索本体は問題ないことがわかる。問題はラッパーレイヤーにある。本番ログで壁に当たるのが最もコストが高い。ローカルでハブを起動して再現できるなら、本番にプッシュしない。 依存関係の問題はビルド時自己チェックに入れて、Actions で爆発させる。
serverInfo.versionはコードバージョンではない(それは mcp ライブラリのバージョン)。本番が新しいコードかどうかを判断するには、 新しいバージョンにしかない文字列を探す。例えばGET /が返す about のテキスト。
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.-