Skip to main content
Glama

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エンドポイント

ツール

豆包検索

https://fluidgender159-hub-mcp.hf.space/doubao/sse

web_search_custom(Custom版、web+image、サイト限定/業界/権威レベルなどの指定に対応)/ web_search_global(Global版、テキストと画像の混合、PDF、ICP限定、短辺/アスペクト比フィルタに対応)

知乎

https://fluidgender159-hub-mcp.hf.space/zhihu/sse

zhihu_search / global_search / zhihu_ask / zhihu_trending

DuckDuckGo

https://fluidgender159-hub-mcp.hf.space/ddg/sse

search / scrape(旧版、HTMLをスクレイピング、DDGのアンチスクレイピングに阻まれやすい)

DuckDuckGo(オリジナルTSブリッジ)

https://fluidgender159-hub-mcp.hf.space/duck/sse

ddg_get_answer / ddg_search / ddg_search_news / ddg_search_images / ddg_search_videos / ddg_fetch_content / ddg_get_suggestions / ddg_get_definition / ddg_convert_currency(hung319/duck-mcp オリジナル、node子プロセスブリッジ、VMチャレンジ解法 + Chrome 3 TLSフィンガープリント、反爬対策)

学術論文

https://fluidgender159-hub-mcp.hf.space/academic/sse

paper_search / paper_download / paper_read(nalkalin/academic-mcp、独立venv子プロセスブリッジ、arXiv/PubMed/PMC/bioRxiv/medRxiv/Semantic Scholar/CrossRef/IACR/CORE など18の学術ソース、キー不要)

エンドポイントの実測ステータス(2026-08-20)

エンドポイント

tools/list

実際の呼び出し

備考

/doubao/sse

✅

✅

無料枠はCustom+Globalで合計500回/月、使い切らないように注意

/zhihu/sse

✅

✅

/academic/sse

✅ 3ツール

✅ 実際の論文を返す

arXivは成功、キー不足のソース(Scopus/WOS/CORE/IEEE…)は警告のみで影響なし

/duck/sse

✅ 9ツール

⚠️ ブリッジは接続、上流がブロック

DDGがHFデータセンターのIPにanti-botチャレンジを返す、コードの問題ではない、IP変更/プロキシが必要

/ddg/sse

✅

⚠️

旧版、反スクレイピングで死にやすい、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

/ddg/search

{query, count?, region?, time_range?, safe_search?}

{items:[{title,url,text}], images:[]}

POST

/ddg/scrape

{url, max_length?}

{urls:[{url,content,metadata:{...}}]}

戻り値はrikkahubの SearchResult / ScrapedResult と完全に同型なので、クライアントはそのままデシリアライズできます。 認証も Authorization: Bearer <DDG_KEY> で、エラー時は {"detail": "..."} を返します。

認証

各MCPは独立したBearerキー(Authorization: Bearer <key>)を持ちます:

MCP

key env

デフォルト値

doubao

DOUBAO_KEY

wei123..

zhihu

ZHIHU_KEY

wei123..

ddg

DDG_KEY

wei123..

duck

DUCK_KEY

wei123..

academic

ACADEMIC_KEY

wei123..

envが設定されていればenvの値を使い、なければデフォルトを使います。GET / のホームページで各エンドポイントの認証設定状況や上流のシークレットが配置されているかを確認できます。

上流のシークレット(HF Space Settings → Secrets に置く、リポジトリには書かない)

env

用途

VOLCENGINE_ARK_API_KEY

火山方舟豆包検索 Custom 版 API キー(必須、Global 版未設定時はこれにフォールバック)

VOLCENGINE_GLOBAL_API_KEY

豆包検索 Global 版専用キー(任意、「APIキー管理-従量課金」で作成、未設定だと Global 版は ARK キーを使い、高確率で 700901 エラー)

ZHIHU_ACCESS_SECRET

知乎オープンプラットフォームの 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 Dockerfile

FROM python:3.12-slim + node/venv インストール + COPY

実際にイメージをビルド

HF Dockerfile

FROM ghcr.io/fuwei99/hub-mcp@sha256:...

既存のイメージを引っ張って実行するだけ

🚨 鉄の掟

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 tz

Actions のログでも入れ子ビルドは一目でわかります:正常なビルドには 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連続のエラー:

エラー

原因

No module named 'pydantic_settings'

fastmcp が必要だが、academic-mcp が宣言していない

cannot import name 'McpError'(Did you mean MCPError? と表示)

mcp 2.0.0 が取得されたが、fastmcp は mcp 1.x の McpError を必要とする(2.0 で MCPError に改名)

cannot import name 'FastMCP' from 'fastmcp' (unknown location)

2段階で pip install -U fastmcp を実行し、パッケージが空の名前空間の残骸になった

修正: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

python:3.12-slim に curl がない

先に apt install curl

bun Permission denied

HF ビルド環境の制限

Node 22 公式 tarball に変更、python tarfile で解凍(tar 内に実行ビットが含まれ、xz-utils も不要)

stdio_client() が env/args エラーを報告

mcp==1.2.0 の旧 API:StdioServerParameters オブジェクトを1つだけ受け取る

旧シグネチャに合わせて単一引数を渡す

academic が FileNotFoundError: [Errno 2] を報告

xlin.xmap_async → ProcessPoolExecutor → multiprocessing.Lock が /dev/shm を必要とする;proot サンドボックスにはない

純粋にローカル環境の制限、HF/docker では正常。ダウンロードディレクトリは別途 ACADEMIC_MCP_DOWNLOAD_PATH=/tmp/papers を設定

ローカル/HF リモートが分岐

両側で並行プッシュ

rebase 後に強制プッシュ;またはデプロイ専用にクリーンな clone を1つ作成

トラブルシューティングの方法論(時間を節約する部分)

  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 不回 → 问题在桥拉子进程那一步
  2. レイヤーごとに特定:ブリッジシェル → 子プロセスが単独で実行できるか → 子プロセスのライブラリ関数を直接呼ぶ。 この例では ArxivSearcher().search() を直接呼ぶと正常で、検索本体は問題ないことがわかる。問題はラッパーレイヤーにある。

  3. 本番ログで壁に当たるのが最もコストが高い。ローカルでハブを起動して再現できるなら、本番にプッシュしない。 依存関係の問題はビルド時自己チェックに入れて、Actions で爆発させる。

  4. serverInfo.version はコードバージョンではない(それは mcp ライブラリのバージョン)。本番が新しいコードかどうかを判断するには、 新しいバージョンにしかない文字列を探す。例えば GET / が返す about のテキスト。

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    A
    maintenance
    An 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.
    302
    MIT
  • F
    license
    Not graded
    quality
    Not graded
    maintenance
    MCP 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
    -
  • A
    license
    Not graded
    quality
    C
    maintenance
    Zero-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.
    11
    Apache 2.0
  • F
    license
    Not graded
    quality
    B
    maintenance
    An extensible MCP hub that exposes internal services (chat, observability, RAG) as namespaced tools via FastMCP, with OpenAPI auto-generation, auth, and resilient error handling.
    -