MCP Hub
title: Pioneer emoji: 🔥 colorFrom: purple colorTo: pink sdk: docker app_port: 7860 pinned: false license: mit
MCP Hub
Un solo HF Space que aloja varios servidores MCP, separados por ruta, cada uno con su propia clave de autenticación.
Reestructurado siguiendo el patrón de plugins de local-mcp-hub: un MCP por archivo .py, main.py los descubre y ensambla automáticamente; no hace falta tocar el archivo principal para añadir un MCP nuevo.
Related MCP server: MCP Hub
Estructura
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-mcpPuente de subprocesos (duck / academic)
Estos dos no están implementados desde cero, sino que levantan el servidor MCP original de aguas arriba como subproceso, se comunican con él por stdio,
y el hub solo hace de proxy de protocolo (reenvío transparente de tools/list y tools/call):
duck: el proyecto original es TypeScript (sandbox de VM para resolver anti-bot challenges + huella TLS de Chrome 134). Portarlo a Python sería demasiado costoso; el proyecto entero está en
duck-mcp/y en la imagen se ejecutadist/index.jscon Node 22.academic: es Python puro, pero sus dependencias (fastmcp) chocan con el
mcp==1.2.0del hub, así que está instalado en un venv independiente:/opt/academic-venv.
La implementación común está en mcps/_stdio_bridge.py; cada llamada abre una sesión nueva y la cierra al terminar.
No es pereza: es lo que pide anyio. El motivo está en el «Archivo de errores #1»; no añadas caché de sesión.
Endpoints
MCP | Endpoint SSE | Herramientas |
Búsqueda Doubao |
|
|
Zhihu |
|
|
DuckDuckGo |
|
|
DuckDuckGo (puente TS original) |
|
|
Artículos académicos |
|
|
Estado real de los endpoints (2026-08-20)
Endpoint | tools/list | Llamada real | Notas |
| ✅ | ✅ | La cuota gratuita de Custom+Global es de 500 usos/mes, no la agotes. |
| ✅ | ✅ | |
| ✅ 3 herramientas | ✅ devuelve artículos reales | arXiv funciona; las fuentes sin clave (Scopus/WOS/CORE/IEEE…) solo dan warning, no rompen nada. |
| ✅ 9 herramientas | ⚠️ el puente conecta, pero el upstream bloquea | DDG devuelve un anti-bot challenge a las IPs del centro de datos de HF; no es un problema de código, hay que cambiar de IP / usar proxy. |
| ✅ | ⚠️ | Versión antigua, el anti-bot la bloquea más fácil; se queda para REST, se puede borrar. |
La trampa de los parámetros de academic
paper_search / paper_download reciben un array de objetos query_list, no un string:
{"query_list": [{"query": "quantum computing", "searcher": "arxiv", "max_results": 2}]}Si omites searcher, busca en todas las fuentes (lento). paper_read usa {"searcher": ..., "paper_id": ...}.
Endpoints REST (para la app Android rikkahub, no MCP)
Método | Ruta | body | Respuesta |
POST |
|
|
|
POST |
|
|
|
La respuesta es totalmente isomórfica con SearchResult / ScrapedResult de rikkahub; el cliente solo tiene que deserializar.
La autenticación también es Authorization: Bearer <DDG_KEY>; en caso de error devuelve {"detail": "..."}.
Autenticación
Cada MCP tiene su propia clave Bearer independiente (Authorization: Bearer <key>):
MCP | key env | Valor por defecto |
doubao |
|
|
zhihu |
|
|
ddg |
|
|
duck |
|
|
academic |
|
|
Si la variable de entorno está configurada, se usa su valor; si no, el valor por defecto. En la página de inicio GET / se puede ver si la autenticación de cada endpoint está configurada y si los secretos de aguas arriba están listos.
Secretos de aguas arriba (ponlos en HF Space Settings → Secrets, no los subas al repositorio)
env | Propósito |
| Clave API de la versión Custom de Búsqueda Doubao de Volcengine Ark (obligatoria; si no configuras la versión Global, se usará esta como respaldo). |
| Clave específica para la versión Global de Búsqueda Doubao (opcional; se crea en «Gestión de claves API - pago por uso». Si no la pones, la versión Global usará la clave ARK y probablemente dé el error 700901). |
| Access Secret de la plataforma abierta de Zhihu. |
Añadir un MCP nuevo
Pon un .py en mcps/; no hace falta tocar 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]:
...No escribas
if __name__ == "__main__": server.run(...)— el puerto y las rutas los gestiona el hub.Para montar varios endpoints con un solo
.py:MOUNTS = {"path1": srv1, "path2": srv2}.Para añadir rutas REST extra (montaje único):
ROUTES = [starlette.Route("/xxx", endpoint=..., methods=["POST"])], se montarán bajo la ruta del plugin.Para importar librerías del mismo directorio:
import _xxx(los archivos que empiezan por guion bajo no se cargan como plugins).Si un plugin falla al importar, solo saldrá en
brokende/; no afecta a los demás.
Ejecución local
pip install -r requirements.txt
uvicorn main:app --port 7860Cadena de compilación y despliegue
El entorno de compilación de HF Space tiene muchas limitaciones (no se puede instalar bun, ni siquiera hay curl), así que no se compila en HF:
改代码 → push GitHub(fuwei99/hub-mcp) → Actions 构建镜像 → 推 GHCR
↓
HF 的 Dockerfile 只 FROM 拉现成镜像Los dos Dockerfile tienen contenido distinto y cada uno se encarga de una cosa:
Ubicación | Contenido | Función |
|
| Compila la imagen real |
|
| Solo tira de la imagen ya compilada |
🚨 Regla de hierro
1. El Dockerfile de HF no debe sincronizarse jamás a GitHub. Si no, las Actions harán un «build en muñeca rusa»: reutilizan la imagen anterior y la vuelven a subir tal cual, sin ejecutar ni un solo
COPY; la imagen siempre será el código viejo, pero el build dirá success. Ya ha pasado dos veces (ver archivo de errores #2). El detonante: si el remoto del repositorio local apunta a HF, no ejecutesgit checkout origin/main -- Dockerfilesobre el Dockerfile, porque arrastrará la versión de HF al local y la subirá a GitHub.2. En HF fija el digest, no uses
:latest. El build de HF cachea el digest antiguo de latest; si la etiqueta no cambia, no vuelve a bajar las capas → el código cambia pero en producción sigue la versión vieja.3. Antes de desplegar, verifica; no te fíes del "success". Saca la capa de código de GHCR y comprueba que los archivos están bien (método abajo); es mucho más rápido que chocarte una y otra vez con los logs de producción.
Flujo estándar para cambiar de imagen
# 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 .aboutVerificación: sacar archivos de GHCR
Sin necesidad de docker, con un simple curl se puede desempaquetar la capa de la imagen (la forma definitiva de saber si el build ha ido bien):
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 tzEn los logs de Actions también se ve de un vistazo el «build en muñeca rusa»: un build normal tiene COPY y tarda 1-2 minutos;
el de muñeca rusa solo tiene resolve ghcr.io/... done + exporting layers, en 2 segundos está.
Archivo de errores
#1 ⭐ El ámbito de cancelación de anyio no puede cruzar tareas (la causa real del cuelgue del puente de subprocesos)
Síntoma: /duck/sse y /academic/sse conectan, initialize responde al instante, pero tools/list
se queda en silencio para siempre — sin error, sin timeout, solo pings en el SSE. Cualquier cliente MCP se queda «colgado».
Pistas que se descartaron (ninguna era la causa): conexión SSE larga, script de prueba, que node no arrancara, contaminación del stdout por el banner (el banner va por stderr, el stdout está limpio).
Causa real: stdio_client() y ClientSession() son contextos anyio ligados a la tarea.
El puente, para ahorrar recursos, hacía __aenter__ en la tarea de la petición A y cacheaba la sesión en una variable global para reutilizarla en la petición B.
Pero en el hub cada conexión SSE es una tarea independiente, así que:
RuntimeError: Attempted to exit cancel scope in a different task than it was entered inEl comportamiento es muy traicionero: initialize responde porque lo responde la propia carcasa del puente, sin tocar el subproceso;
en cuanto llega tools/list, que sí necesita reenviar al subproceso, muere en el ámbito de cancelación entre tareas.
Reproducción (script local de 30 líneas, sin desplegar):
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())Solución: mcps/_stdio_bridge.py — cada list_tools/call_tool abre el subproceso dentro de la tarea actual,
lo cierra con async with y lo cierra al terminar; solo se cachea la descripción de herramientas (datos puros, se pueden compartir entre tareas).
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)No intentes «optimizar» con una sesión compartida. Si de verdad quieres ir más rápido, la forma correcta es un worker task de larga duración + cola, y que todo el IO se haga dentro de esa tarea, no pasar el objeto de contexto entre tareas.
Lección extra: ClientSession(read, write) sin __aenter__ también se cuelga —
la tarea de fondo «leer stdout → despachar respuesta» solo se lanza dentro de __aenter__;
si no entras en el contexto, las peticiones enviadas se quedan sin respuesta.
#2 ⭐ Build en muñeca rusa (la imagen siempre es código viejo, pero el build dice success)
Síntoma: cambias el código, Actions da success, HF reconstruye y queda RUNNING, pero el comportamiento en producción no cambia. Se sospechó de la caché de HF, de GHCR, de las capas... y no era nada de eso.
Cómo se localizó: se sacó la capa de COPY mcps/ de GHCR y se hizo tar tzf — el _stdio_bridge.py recién añadido
ni siquiera estaba en la imagen, aunque en GitHub sí estaba. Y al mirar los logs de Actions:
#1 transferring dockerfile: 647B ← 完整版有 2.7KB
#5 resolve ghcr.io/fuwei99/hub-mcp@sha256:0799864b... done
#7 exporting layers done ← 全程 2 秒,零 COPYCausa real: el Dockerfile del repositorio de GitHub se había convertido en el de HF,
FROM ghcr.io/fuwei99/hub-mcp@sha256:... — las Actions reutilizaban la imagen anterior y la volvían a subir tal cual.
Cómo se coló: el remoto del repositorio local era HF, se ejecutó
git checkout origin/main -- Dockerfile, que trajo la versión de HF al área de trabajo,
y al hacer push a GitHub se subió junto.
A prueba de tontos (ya añadido a .github/workflows/build.yml; si vuelve a pasar, el build falla en el acto):
- 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; }Además, en el Dockerfile se ha añadido una autocomprobación en tiempo de compilación: test -f mcps/_stdio_bridge.py || exit 1.
#3 El infierno de dependencias de academic-mcp
El academic-mcp==0.1.7 de aguas arriba no fija el límite superior de dependencias, y la combinación que instala está rota. Tres errores seguidos:
Error | Causa |
| fastmcp lo necesita, pero academic-mcp no lo declara. |
| Se instaló |
| Al hacer |
Solución: instalar todo de una vez, fijar el límite superior explícitamente y hacer una autocomprobación de import en tiempo de compilación:
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')"Combinación probada: academic-mcp 0.1.7 + fastmcp 3.4.7 (o 2.14.1) + mcp 1.29.0
pydantic-settings 2.15.0.
Lección: al actualizar dependencias, no hagas pip install en dos pasos y luego pip install -U; instala todo de una vez para que el resolvedor decida de forma unificada.
La combinación de dependencias hay que probarla en un venv local antes de escribirla en el Dockerfile, y poner la autocomprobación de import en la fase de compilación:
si algo falla, que falle el build, no que salte en los logs de producción.
#4 Otros
Síntoma | Causa | Solución |
La descarga de bun da exit 127 |
| Primero |
bun | Limitaciones del entorno de compilación de HF | Usar el tarball oficial de Node 22 y descomprimir con |
| API antigua de | Pasar un solo argumento según la firma antigua. |
academic da |
| Es una limitación del entorno local; en HF/docker funciona. En el directorio de descarga, poner |
Divergencia local/remoto de HF | Push en paralelo en ambos lados | Hacer rebase y forzar el push; o clonar limpio para desplegar. |
Metodología de resolución de problemas (la parte que ahorra tiempo)
No uses un cliente MCP de Python para depurar cuelgues — él mismo se cuelga y no ves nada. Usa curl a mano y mira quién no responde:
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 不回 → 问题在桥拉子进程那一步Localiza por capas: carcasa del puente → ¿puede ejecutarse el subproceso por separado? → llama directamente a la librería del subproceso. En este caso,
ArxivSearcher().search()funciona si se llama directo, así que el buscador está bien; el problema está en la capa de empaquetado.Chocarse contra los logs de producción es lo más caro. Si puedes reproducirlo en local con el hub, no lo pruebes en producción; los problemas de dependencias se meten en la autocomprobación de la fase de compilación para que revienten en las Actions.
serverInfo.versionno es la versión del código (es la versión de la librería mcp). Para saber si en producción hay código nuevo, busca una cadena que solo exista en la versión nueva, como el texto de about que devuelveGET /.
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