MCP Hub
A multi-MCP server hub that hosts several independent MCP servers (e.g., DuckDuckGo, Zhihu, Doubao, Academic) with per-path authentication, deployable on Hugging Face Spaces.
README
title: Pioneer emoji: 🔥 colorFrom: purple colorTo: pink sdk: docker app_port: 7860 pinned: false license: mit
MCP Hub
一个 HF Space 挂多个 MCP Server,路径区分,各自独立鉴权 key。
按 local-mcp-hub 的插件玩法重构:一个 MCP 一个 py,main.py 自动发现装配,加新 MCP 不用改主文件。
结构
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 server 当子进程拉起来,用 stdio 跟它说话,
hub 只做协议转发(tools/list、tools/call 原样透传):
- duck:上游是 TS 项目(VM 沙箱解 anti-bot challenge + Chrome134 TLS 指纹),
移植成 Python 成本太高,整个项目塞进
duck-mcp/,镜像里用 node 22 跑dist/index.js。 - academic:纯 Python,但依赖(fastmcp)跟 hub 的
mcp==1.2.0打架, 所以装在独立 venv/opt/academic-venv里隔离。
公共实现在 mcps/_stdio_bridge.py,每次调用起一条独立会话、用完即关——
这不是偷懒,是被 anyio 逼的,原因见「踩坑档案 #1」,别去加 session 缓存。
端点
| 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 挑战解法 + Chrome134 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 学术源,免 key) |
端点实测状态(2026-08-20)
| 端点 | tools/list | 实际调用 | 备注 |
|---|---|---|---|
/doubao/sse |
✅ | ✅ | 免费额度 Custom+Global 共用 500 次/月,别耗尽 |
/zhihu/sse |
✅ | ✅ | |
/academic/sse |
✅ 3 工具 | ✅ 真论文返回 | arXiv 走通,缺 key 的源(Scopus/WOS/CORE/IEEE…)只是警告不影响 |
/duck/sse |
✅ 9 工具 | ⚠️ 桥通、上游被拦 | DDG 对 HF 数据中心 IP 返回 anti-bot challenge,非代码问题,要换 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 安卓端,非 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 key(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 / 首页可看各端点鉴权是否配置、上游 secret 有没有就位。
上游 Secrets(放 HF Space Settings → Secrets,勿写进仓库)
| env | 用途 |
|---|---|
VOLCENGINE_ARK_API_KEY |
火山方舟豆包搜索 Custom 版 API key(必配,Global 版未配时回落用它) |
VOLCENGINE_GLOBAL_API_KEY |
豆包搜索 Global 版专用 key(可选,去「API Key管理-按量后付费」创建,不配则 Global 版会用 ARK key 并大概率报 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(...)——端口和路由归 hub 管。 - 一个 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)。 具体地雷:本地仓库 remote 指向 HF 时,别对 Dockerfile 执行git checkout origin/main -- Dockerfile,会把 HF 版拉进本地再一起推去 GitHub。2. HF 侧钉 digest,不要用
:latest。 HF 构建会缓存 latest 的旧 digest,tag 不变就不重新拉层 → 代码改了线上还是旧的。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 不能跨 task(子进程桥卡死真因)
现象:/duck/sse、/academic/sse 连得上、initialize 秒回,但 tools/list
永久静默 —— 不报错、不超时、SSE 里只有 ping。任何 MCP 客户端接上去都是"卡死"。
误判过的方向(都不是原因):SSE 长连接、测试脚本、node 起不来、 banner 污染 stdout(banner 走的是 stderr,stdout 干净)。
真因:stdio_client() 和 ClientSession() 都是 task-bound 的 anyio 上下文。
桥当初为了省开销,在 A 请求的 task 里 __aenter__ 后把 session 缓存到全局给 B 请求复用。
而 hub 里每个 SSE 连接是独立 task,于是:
RuntimeError: Attempted to exit cancel scope in a different task than it was entered in
表现极其阴险:initialize 能回是因为那是桥壳自己答的,根本没碰子进程;
一到 tools/list 需要真子进程转发,就死在跨 task 的 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 都在当前 task 内
开子进程、async with 闭合、用完即关;只缓存工具描述(纯数据,可跨 task)。
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)
别去"优化"成共享 session。真要提速,正确做法是起一个专属长驻 worker task + 队列,所有 IO 都在那个 task 内做,而不是把上下文对象跨 task 传。
顺带的教训:ClientSession(read, write) 光 new 不 __aenter__ 也会挂 ——
后台的「读 stdout → 派发响应」task 是在 __aenter__ 里才启动的,
不进上下文就等于发出去的请求没人收回复。
#2 ⭐ 套娃构建(镜像永远是旧代码,构建却 success)
现象:改完代码、Actions success、HF 重建 RUNNING,但线上行为一点没变。 反复怀疑 HF 缓存、GHCR 缓存、layer 缓存,全不是。
定位手段:从 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 版拉进工作区,
之后 push 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 没锁依赖上限,装出来的组合是坏的。三连报错:
| 报错 | 原因 |
|---|---|
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) |
分两步 pip install -U fastmcp 把包装成了空壳命名空间残包 |
修复:一条命令装齐、显式锁上限,并在构建期做 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。
教训:升级依赖别分两步 pip install 再 pip install -U,一次装齐让解析器统一决策;
依赖组合务必本地 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 对象 |
按旧签名传单参数 |
academic 报 FileNotFoundError: [Errno 2] |
xlin.xmap_async → ProcessPoolExecutor → multiprocessing.Lock 需要 /dev/shm;proot 沙箱里没有 |
纯本地环境限制,HF/docker 正常。下载目录另设 ACADEMIC_MCP_DOWNLOAD_PATH=/tmp/papers |
| 本地/HF 远端分叉 | 两边并行推送 | rebase 后强推;或干净 clone 一份专门用于部署 |
排障方法论(省时间的部分)
-
别用 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()直接调是好的,说明搜索本体没问题,锅在包装层。 -
在线上日志里撞墙最贵。能本地起 hub 复现的,绝不推线上试; 依赖问题放进构建期自检,让它在 Actions 里就炸。
-
serverInfo.version不是代码版本(那是 mcp 库版本)。判断线上是不是新代码, 要找只有新版才有的字符串,比如GET /返回的 about 文案。
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。
mcp-server-qdrant
这个仓库展示了如何为向量搜索引擎 Qdrant 创建一个 MCP (Managed Control Plane) 服务器的示例。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。