Skip to main content
Glama

MCP Hub

An HF Space hosting multiple MCP Servers, distinguished by path, each with independent authentication keys. Refactored following the local-mcp-hub plugin pattern: one MCP per py, with main.py auto-discovering and assembling them, so adding a new MCP requires no changes to the main file.

Structure

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

Subprocess Bridge (duck / academic)

These two aren't implemented from scratch; instead, they launch the upstream original MCP server as a subprocess and talk to it over stdio. The hub only does protocol forwarding (tools/list, tools/call passed through as-is):

  • duck: Upstream is a TS project (VM sandbox solving anti-bot challenges + Chrome134 TLS fingerprinting). Porting it to Python would be too costly, so the whole project is stuffed into duck-mcp/, run with node 22 in the image via dist/index.js.

  • academic: Pure Python, but its dependencies (fastmcp) conflict with the hub's mcp==1.2.0, so it's installed in a separate venv at /opt/academic-venv for isolation.

The shared implementation lives in mcps/_stdio_bridge.py. Each call spawns an independent session and closes it immediately after use — this isn't laziness; it's forced by anyio. See "Pitfall Archive #1" for why. Don't add session caching.

Related MCP server: MCP Hub

Endpoints

MCP

SSE Endpoint

Tools

Doubao

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

web_search_custom (Custom version, web+image, supports site restriction/industry/authority level, etc.) / web_search_global (Global version, mixed text+image, supports pdf, ICP restriction, short-side/aspect-ratio filtering)

Zhihu

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 (legacy, scrapes HTML, easily blocked by DDG anti-scraping)

DuckDuckGo (original TS bridge)

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 original, node subprocess bridge, VM challenge solver + Chrome134 TLS fingerprint, anti-scraping)

Academic Papers

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

paper_search / paper_download / paper_read (nalkalin/academic-mcp, isolated venv subprocess bridge, arXiv/PubMed/PMC/bioRxiv/medRxiv/Semantic Scholar/CrossRef/IACR/CORE and 18 other academic sources, no key required)

Endpoint Live Status (2026-08-20)

Endpoint

tools/list

Actual Call

Notes

/doubao/sse

Free tier Custom+Global share 500 calls/month, don't burn through it.

/zhihu/sse

/academic/sse

✅ 3 tools

✅ Real papers returned

arXiv works; sources missing keys (Scopus/WOS/CORE/IEEE…) only warn, non-fatal

/duck/sse

✅ 9 tools

⚠️ Bridge up, upstream blocked

DDG returns anti-bot challenge to HF datacenter IPs, not a code issue, need to change IP / use a proxy

/ddg/sse

⚠️

Legacy, more easily blocked by anti-scraping; kept for REST, consider removing

academic Parameter Gotchas

paper_search / paper_download take a query_list object array, not a string:

{"query_list": [{"query": "quantum computing", "searcher": "arxiv", "max_results": 2}]}

Omitting searcher = search all sources (slow). paper_read takes {"searcher": ..., "paper_id": ...}.

REST Endpoints (for the rikkahub Android client, not MCP)

Method

Path

body

Response

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:{...}}]}

The response body is fully isomorphic with rikkahub's SearchResult / ScrapedResult, so the client can deserialize directly. Auth is also Authorization: Bearer <DDG_KEY>; on error it returns {"detail": "..."}.

Authentication

Each MCP has an independent Bearer key (Authorization: Bearer <key>):

MCP

key env

Default

doubao

DOUBAO_KEY

wei123..

zhihu

ZHIHU_KEY

wei123..

ddg

DDG_KEY

wei123..

duck

DUCK_KEY

wei123..

academic

ACADEMIC_KEY

wei123..

If the env var is set, its value is used; otherwise the default applies. The GET / homepage shows whether each endpoint's auth is configured and whether upstream secrets are in place.

Upstream Secrets (put in HF Space Settings → Secrets, never commit to the repo)

env

Purpose

VOLCENGINE_ARK_API_KEY

Volcano Ark Doubao Search Custom version API key (required; Global version falls back to it if not configured)

VOLCENGINE_GLOBAL_API_KEY

Dedicated key for Doubao Search Global version (optional; create under "API Key Management - Pay-as-you-go". If unset, Global uses the ARK key and will likely error 700901)

ZHIHU_ACCESS_SECRET

Zhihu Open Platform Access Secret

Adding a New MCP

Drop a py file into mcps/no changes needed in 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]:
    ...
  • Don't write if __name__ == "__main__": server.run(...) — the port and routes are managed by the hub.

  • To mount multiple endpoints from one py: MOUNTS = {"path1": srv1, "path2": srv2}.

  • To add extra REST routes (single mount): ROUTES = [starlette.Route("/xxx", endpoint=..., methods=["POST"])], mounted under this plugin's path.

  • To reference a library file in the same directory: import _xxx (files starting with underscore aren't loaded as plugins).

  • If a single plugin fails to import, it only shows up in the broken list at / and doesn't affect other plugins.

Running Locally

pip install -r requirements.txt
uvicorn main:app --port 7860

Build & Deploy Pipeline

HF Space's build environment is heavily restricted (can't install bun, no curl), so we don't build inside HF:

改代码 → push GitHub(fuwei99/hub-mcp) → Actions 构建镜像 → 推 GHCR
                                                              ↓
                                    HF 的 Dockerfile 只 FROM 拉现成镜像

The two Dockerfiles have different contents, each handling its own job:

Location

Content

Purpose

GitHub Dockerfile

FROM python:3.12-slim + install node/venv + COPY

Actually builds the image

HF Dockerfile

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

Only pulls a prebuilt image and runs it

🚨 Iron Rules

1. Never sync the HF Dockerfile back to GitHub. Otherwise Actions will do a "Matryoshka build": re-pushing the previous image as-is, with COPY never executing. The image always contains old code, yet the build shows success. Already bitten twice (see Pitfall Archive #2). The exact landmine: when the local repo's remote points to HF, don't run git checkout origin/main -- Dockerfile on the Dockerfile, or you'll pull the HF version locally and push it to GitHub along with everything else.

2. Pin the digest on the HF side; don't use :latest. HF's build caches the old digest of latest; if the tag doesn't change it won't re-pull layers → code changes but production still runs the old version.

3. Inspect the image before deploying; don't blindly trust "success". Pull the code layer out of GHCR and check the files (method below) — much faster than hitting your head against the wall in production logs.

Standard Flow for Swapping Images

# 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

Inspection: Pulling Files Out of GHCR

No docker needed — pure curl is enough to unpack the image layers (the ultimate way to verify a build actually took effect):

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

You can also spot a Matryoshka build at a glance in the Actions logs: a normal build has COPY and takes 1–2 minutes; a Matryoshka build only shows resolve ghcr.io/... done + exporting layers, done in 2 seconds.


Pitfall Archive

#1 ⭐ anyio cancel scopes can't cross tasks (the real cause of the subprocess bridge hanging)

Symptom: /duck/sse and /academic/sse connect fine, initialize responds instantly, but tools/list hangs silently forever — no error, no timeout, only pings in the SSE stream. Any MCP client connecting appears "frozen."

Dead ends investigated (none were the cause): SSE keep-alive, test scripts, node failing to start, banner polluting stdout (the banner goes to stderr; stdout is clean).

Root cause: stdio_client() and ClientSession() are both task-bound anyio contexts. To save overhead, the bridge used to __aenter__ in request A's task and cache the session globally for request B to reuse. But since each SSE connection in the hub is an independent task, this happens:

RuntimeError: Attempted to exit cancel scope in a different task than it was entered in

The behavior is insidious: initialize responds because the bridge shell answers it directly without ever touching the subprocess; the moment tools/list needs real subprocess forwarding, it dies on the cross-task cancel scope.

Reproduction (30-line local script, no deployment needed):

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 — every list_tools/call_tool spawns a subprocess within the current task, closes it with async with, and tears it down immediately; only the tool descriptions (pure data, safe across tasks) are cached.

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)

Don't "optimize" this into a shared session. If you really need speed, the right approach is a dedicated long-running worker task + queue, with all IO happening inside that task — not passing context objects across tasks.

Bonus lesson: ClientSession(read, write) will also hang if you only new it without __aenter__ — the background "read stdout → dispatch response" task only starts inside __aenter__. If you never enter the context, your requests go out with no one listening for replies.

#2 ⭐ Matryoshka Build (image always has old code, yet the build succeeds)

Symptom: Code changed, Actions succeeds, HF rebuilds to RUNNING, but production behavior doesn't change at all. Suspected HF cache, GHCR cache, layer cache — none of them.

How it was found: Pulled the COPY mcps/ layer out of GHCR and ran tar tzf — the newly added _stdio_bridge.py wasn't in the image at all, even though it was clearly on GitHub. Then looked at the Actions log:

#1 transferring dockerfile: 647B          ← 完整版有 2.7KB
#5 resolve ghcr.io/fuwei99/hub-mcp@sha256:0799864b... done
#7 exporting layers done                  ← 全程 2 秒,零 COPY

Root cause: The Dockerfile in the GitHub repo had become the HF version FROM ghcr.io/fuwei99/hub-mcp@sha256:... — Actions was re-pushing the old image as-is.

How it got in: The local repo's remote was HF, and running git checkout origin/main -- Dockerfile pulled the HF version into the working tree, which then got pushed to GitHub along with everything else.

Foolproofing (added to .github/workflows/build.yml; the build fails immediately if it happens again):

- 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; }

Also added a build-time self-check in the Dockerfile: test -f mcps/_stdio_bridge.py || exit 1.

#3 academic-mcp's Dependency Hell

Upstream academic-mcp==0.1.7 doesn't pin upper bounds on dependencies, so the installed combination is broken. A triple whammy of errors:

Error

Cause

No module named 'pydantic_settings'

fastmcp needs it, but academic-mcp doesn't declare it

cannot import name 'McpError' (hint: Did you mean MCPError?)

Pulled mcp 2.0.0, but fastmcp needs McpError from mcp 1.x (renamed MCPError in 2.0)

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

A two-step pip install -U fastmcp turned the package into an empty namespace stub

Fix: install everything in one command, explicitly pin upper bounds, and add an import self-check at build time:

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')"

Tested working combination: academic-mcp 0.1.7 + fastmcp 3.4.7 (or 2.14.1) + mcp 1.29.0

  • pydantic-settings 2.15.0.

Lesson: Don't upgrade dependencies with two separate pip install steps followed by pip install -U; install everything at once so the resolver makes a unified decision. Always test the dependency combination in a local venv before writing it into the Dockerfile, and put the import self-check into the build phase — if something's wrong, the build fails, instead of waiting for errors in production logs.

#4 Miscellaneous

Symptom

Cause

Fix

bun download exits 127

python:3.12-slim has no curl

apt install curl first

bun Permission denied

HF build environment restriction

Switched to the official Node 22 tarball, extracted with python tarfile (the tar has the exec bit baked in, no need for xz-utils)

stdio_client() errors on env/args

mcp==1.2.0 old API: only accepts a single StdioServerParameters object

Pass a single argument per the old signature

academic reports FileNotFoundError: [Errno 2]

xlin.xmap_asyncProcessPoolExecutormultiprocessing.Lock needs /dev/shm; not present in the proot sandbox

Local-environment-only limitation; fine on HF/docker. Set download dir to ACADEMIC_MCP_DOWNLOAD_PATH=/tmp/papers

Local/HF remote drift

Pushing to both in parallel

Force-push after rebase; or do a clean clone dedicated to deployment

Troubleshooting Methodology (the time-savers)

  1. Don't debug hangs with a Python MCP client — it hangs too, and you can't tell where it's stuck. Use curl to speak the protocol by hand and watch frame by frame who stops responding:

    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. Isolate by layer: bridge shell → can the subprocess run standalone → call the subprocess library function directly. In this case, calling ArxivSearcher().search() directly worked, which meant the search itself was fine and the bug was in the wrapper layer.

  3. Hitting your head against production logs is the most expensive option. If you can reproduce it by running the hub locally, never test on production; put dependency checks into the build phase so they blow up in Actions instead.

  4. serverInfo.version is not the code version (that's the mcp library version). To tell whether production is running new code, look for a string that only exists in the new version, like the about text returned by GET /.

F
license - not found
Not graded
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

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.
    276
    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.
    781
  • A
    license
    Not graded
    quality
    A
    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.
    10
    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.

View all related MCP servers

Related MCP Connectors

  • Hosted MCP server for LLM cost estimation, model comparison, and budget-aware routing.

  • MCP Hub: AI service discovery, per-user OAuth, and multi-service workflow orchestration

  • MCP server for secureFlows: token-free URL builders and integration-linting tools for AI agents.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/fuwei99/hub-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server