mcp-hub

mcp-hub

A hub that serves multiple MCP servers from a single container over HTTPS, with OAuth 2.1 authentication and path-based routing.

Category
访问服务器

README

mcp-hub

CI Container License: MIT

Serve many stdio MCP servers from one container, published over HTTPS for Claude Web custom connectors, Claude Code and any other Streamable-HTTP MCP client — with a built-in OAuth 2.1 login protected by a single password.

Claude ──TLS──> reverse proxy ──> mcp-hub (one Node process)
                                   ├─ OAuth 2.1 AS (DCR, PKCE, password login)
                                   ├─ /<name>, /<name>/mcp   one path per server
                                   ├─ /hub                   4 meta-tools for all servers
                                   └─ supervisor: one stdio child per server, auto-restart

Why

Wrapping each stdio MCP server in its own auth-proxy container costs a full image, an OAuth stack, a hostname and a compose stack per server. mcp-hub replaces N containers with one process:

  • Config is exactly Claude Code's mcpServers format — copy entries 1:1.
  • Path-based routing: https://host/paperless, https://host/homeassistant, …
  • /hub aggregate: register a single connector and reach every server through 4 meta-tools (list_servers, list_tools, get_tool_schema, call_tool) without flooding the model context with N×tools schemas.
  • Supervision: children are spawned at boot, pinged, and restarted with exponential backoff when they die. A down server answers 503, not silence.
  • Hot reload: edits to mcp.json start/stop/restart only the affected servers.
  • Stateless Streamable HTTP: no session state, so claude.ai's reconnect-without-DELETE behaviour cannot leak processes or memory.
  • No database: state is one JSON file plus a JWT key under /data.

Configuration

/config/mcp.json — identical to Claude Code (${VAR} expands from the container environment; unknown fields are ignored by Claude Code, so the file stays interchangeable):

{
  "mcpServers": {
    "paperless": {
      "command": "npx",
      "args": ["-y", "paperless-mcp"],
      "env": { "PAPERLESS_API_TOKEN": "${PAPERLESS_API_TOKEN}" }
    },
    "homeassistant": {
      "type": "http",
      "url": "http://homeassistant:8123/api/mcp",
      "headers": { "Authorization": "Bearer ${HA_TOKEN}" }
    },
    "private-thing": { "command": "uvx", "args": ["some-mcp"], "hub": false }
  }
}

Stdio servers (command/args/env) are spawned as supervised child processes. Remote servers (type: "http" or "sse" with url and optional headers) are connected as MCP clients with the configured headers injected on every request — the same supervision (ping, backoff reconnect, hot reload) applies. Upstreams that require their own interactive OAuth cannot be configured with static headers; bridge those with an mcp-remote stdio entry and persist its token cache (MCP_REMOTE_CONFIG_DIR) under /data. "hub": false hides a server from the /hub aggregate; its own path keeps working. Reserved names: mcp, hub, authorize, token, register, login, consent, health, revoke.

Environment

Variable Required Description
EXTERNAL_URL yes Public base URL, e.g. https://mcp.example.net (no path)
PASSWORD_HASH one of bcrypt hash of the login password (htpasswd -bnBC 10 "" 'pw' | tr -d ':\n')
PASSWORD one of plain-text alternative to PASSWORD_HASH
TRUSTED_PROXIES no comma-separated IPs/CIDRs allowed to set X-Forwarded-* (see below)
PORT no listen port (default 80 in the image, 3000 outside)
CONFIG_PATH no default /config/mcp.json
DATA_PATH no default /data
LOG_FILE no additionally mirror all log output into this file, e.g. /data/mcp-hub.log (see below)

/data holds the Ed25519 JWT key, registered OAuth clients, approvals and refresh tokens. Mount it as a volume — recreating it invalidates every connector authorization.

TRUSTED_PROXIES decides what req.ip is, and therefore what the login rate limiter counts. List only your own reverse proxy, and make sure it overwrites X-Forwarded-For rather than appending to it — otherwise a client can supply its own address and rotate it to sidestep the per-IP limit. If the variable is unset, every request appears to come from the proxy and per-IP limiting degrades to a single global counter (the hub logs a warning at startup). A global cap of 100 failures per 15 minutes applies either way.

Running

Option A — prebuilt image from GHCR (recommended)

Published on every push to main and every vX.Y.Z release tag, for linux/amd64 and linux/arm64. Browse the versions on the package page.

docker pull ghcr.io/ni-c/mcp-hub:latest

Tags: latest (tip of main), X.Y.Z and X.Y (releases), and sha-<commit> for a specific build.

With compose, copy the example and point it at the image instead of building:

services:
  mcp-hub:
    image: ghcr.io/ni-c/mcp-hub:latest   # replaces `build: .`
    # ...rest of docker-compose.example.yml unchanged
cp docker-compose.example.yml docker-compose.yml   # adjust, swap build → image
cp mcp.json.example mcp.json                        # adjust
mkdir -p data && sudo chown -R 1000:1000 data       # container runs as uid 1000
docker compose up -d

Or without compose:

mkdir -p data && sudo chown -R 1000:1000 data       # container runs as uid 1000
docker run -d --name mcp-hub \
  -p 127.0.0.1:7690:80 \
  -e EXTERNAL_URL="https://mcp.example.net" \
  -e PASSWORD_HASH="$(htpasswd -bnBC 10 '' 'yourpassword' | tr -d ':\n')" \
  -e TRUSTED_PROXIES="192.168.1.0/24" \
  -v "$PWD/mcp.json:/config/mcp.json:ro" \
  -v "$PWD/data:/data" \
  ghcr.io/ni-c/mcp-hub:latest

Update to a newer image with docker compose pull && docker compose up -d (or docker pull …, then recreate the container).

Option B — build from source

cp docker-compose.example.yml docker-compose.yml   # adjust
cp mcp.json.example mcp.json                        # adjust
docker compose up -d --build

Reverse-proxy requirements: TLS termination, WebSockets/SSE allowed (proxy buffering off, long read timeouts), and pass X-Forwarded-Proto/Host.

Connect from Claude Web: add a custom connector with URL https://<host>/hub (or https://<host>/<name>/mcp for one server), log in once with the password. Claude Code: claude mcp add -t http name https://<host>/<name>/mcp.

Each client is confirmed once. Entering the password approves the client that asked; while a login session is still valid, a client you have not seen before gets an explicit Approve / Deny page instead of a code. Approved clients reconnect silently from then on. To withdraw an approval, stop the container, remove the entry from approvals in /data/state.json and start it again — the client then has to be confirmed the next time it connects.

Endpoints

Path Auth Purpose
/<name>, /<name>/mcp Bearer Streamable HTTP endpoint of one server
/hub Bearer aggregate endpoint with the 4 meta-tools
/health none per-server status (200 all up / 503 degraded)
/authorize, /token, /register, /login, /consent, /revoke — OAuth 2.1 + DCR
/.well-known/oauth-authorization-server[/…] none RFC 8414 metadata
/.well-known/oauth-protected-resource[/…] none RFC 9728 metadata (path-scoped)

Notes & limitations

  • Stateless transport: server-initiated notifications (listChanged, subscriptions, sampling) are not delivered to clients. Tool/resource/prompt request-response works fully; the hub's tool cache does follow tools/list_changed internally.
  • Access tokens are self-contained 24 h JWTs and cannot be revoked individually; refresh tokens rotate and can be revoked. Replaying a token that was already rotated away revokes its whole chain, and a refresh cannot ask for more scope than the original grant.
  • Upstream auth is fully decoupled from the hub's own OAuth: an expired upstream token just marks that one server down (503 on its path, visible in /health) — clients never see the upstream's 401.
  • One login secures everything: any valid token may call every server. What a token is not is automatic — registration is open (as the MCP spec intends), so a client only ever receives codes after you confirmed it, and only at the redirect target you confirmed it for.
  • Failed logins are rate-limited (10/15 min per IP) and logged as mcp-hub: authentication failure from <ip> for fail2ban.

Logging to a file for fail2ban

LOG_FILE=/data/mcp-hub.log mirrors every log line into that file, one line per entry with an ISO-8601 UTC prefix, while leaving the console output alone — so docker logs keeps working. A jail then reads the file directly:

# /etc/fail2ban/filter.d/mcp-hub-auth.conf
[Definition]
failregex = mcp-hub: authentication failure from <HOST>\s*$
            mcp-hub: login rate limit exceeded from <HOST>\s*$
            mcp-hub: consent with an invalid CSRF token from <HOST>\s*$
ignoreregex =

Only the hub's own lines are mirrored — the stdio children inherit stderr directly, so their output stays in the container log and the file stays small. Rotate it with logrotate (copytruncate, since the hub holds the file open).

Why not read the container's own logs instead: the Docker json-file path contains the container ID and changes on every recreate, and the journald driver maps all stderr to priority err — since an MCP server must keep stdout free for the protocol and therefore logs to stderr, every ordinary line would show up as a system error and drown out host monitoring.

Bans belong in the DOCKER-USER chain (banaction = iptables-allports) when the hub is published through a container-based reverse proxy: that traffic arrives via DNAT and FORWARD, and never passes INPUT.

Development

npm install
npm test           # vitest: config, OAuth flow, proxy E2E, hub, hot reload
npm run dev        # tsx, needs EXTERNAL_URL/PASSWORD/CONFIG_PATH/DATA_PATH

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选