MCP Hub
Allows searching, downloading, and reading academic papers from arXiv through the academic MCP.
Provides DuckDuckGo search tools including web, news, images, videos, content fetching, suggestions, definitions, and currency conversion, plus an older search/scrape endpoint.
Allows searching, downloading, and reading academic papers from IEEE through the academic MCP.
Allows searching, downloading, and reading academic papers from PubMed through the academic MCP.
Allows searching, downloading, and reading academic papers from Scopus through the academic MCP.
Allows searching, downloading, and reading academic papers from Semantic Scholar through the academic MCP.
Provides Zhihu search, global search, asking questions, and trending topic tools.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@MCP HubFind recent papers on quantum computing"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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-mcpSubprocess 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 viadist/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-venvfor 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 |
|
|
Zhihu |
|
|
DuckDuckGo |
|
|
DuckDuckGo (original TS bridge) |
|
|
Academic Papers |
|
|
Endpoint Live Status (2026-08-20)
Endpoint | tools/list | Actual Call | Notes |
| ✅ | ✅ | Free tier Custom+Global share 500 calls/month, don't burn through it. |
| ✅ | ✅ | |
| ✅ 3 tools | ✅ Real papers returned | arXiv works; sources missing keys (Scopus/WOS/CORE/IEEE…) only warn, non-fatal |
| ✅ 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 |
| ✅ | ⚠️ | 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 |
|
|
|
POST |
|
|
|
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 |
|
|
zhihu |
|
|
ddg |
|
|
duck |
|
|
academic |
|
|
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 |
| Volcano Ark Doubao Search Custom version API key (required; Global version falls back to it if not configured) |
| 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 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
brokenlist at/and doesn't affect other plugins.
Running Locally
pip install -r requirements.txt
uvicorn main:app --port 7860Build & 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 |
| Actually builds the image |
HF |
| 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
COPYnever 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 rungit checkout origin/main -- Dockerfileon 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 .aboutInspection: 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 tzYou 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 inThe 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 秒,零 COPYRoot 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 |
| fastmcp needs it, but academic-mcp doesn't declare it |
| Pulled |
| A two-step |
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 |
|
|
bun | HF build environment restriction | Switched to the official Node 22 tarball, extracted with |
|
| Pass a single argument per the old signature |
academic reports |
| Local-environment-only limitation; fine on HF/docker. Set download dir to |
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)
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 不回 → 问题在桥拉子进程那一步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.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.
serverInfo.versionis 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 byGET /.
This server cannot be installed
Maintenance
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
- 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.276MIT
- 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.781
- AlicenseNot gradedqualityAmaintenanceZero-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.10Apache 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.
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.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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