FAQ & troubleshooting
Startup
A server's path returns 503 right after starting
Expected, for a while. Children are started in the background and their paths answer 503 until the MCP handshake completes. A server that downloads a package on first run can take minutes on a slow machine.
Check docker logs mcp-hub and /health. If the state stays down and the restarts counter climbs, that child is crash-looping — its own stderr in the container log will say why.
The hub will not start: Undefined environment variable in config
${VAR} may expand to an empty value, but a variable that is not defined at all aborts parsing of the whole file — one entry takes every server with it.
Pass the variable through explicitly in your Compose file, even as an empty string:
environment:
SOME_TOKEN: "${SOME_TOKEN:-}"EACCES writing /data/jwt-key.pem
The container runs as uid 1000 and a bind-mounted directory keeps the host's ownership:
sudo chown -R 1000:1000 ./datachown in the Dockerfile only affects the image layer, not your bind mount. A named volume inherits the image's ownership and avoids this entirely.
The hub logs a warning about TRUSTED_PROXIES
It is unset, so every request appears to come from the reverse proxy and per-IP rate limiting collapses into one global counter. Set it to your proxy's address — see Security.
Authorization
The client is redirected to http://… instead of https://…
EXTERNAL_URL must be the exact public HTTPS origin, with no trailing path, and the proxy has to pass X-Forwarded-Proto. Every OAuth metadata document and redirect is built from that value.
Every connector suddenly has to authorize again
Three causes, in order of likelihood:
/datawas recreated. The JWT key and all client records live there. A fresh directory means a fresh authorization server as far as clients are concerned.- You upgraded to 0.5.0 or later. Resource binding is on by default now, which intentionally retires tokens issued without a resource. It is a one-time cost;
RESOURCE_BOUND_TOKENS=falsepostpones it. - A client was revoked with
mcp-hub-admin clients revoke.
I get an Approve / Deny page instead of the password prompt
That is the intended behaviour when a login session is still valid (30 minutes) and a client the hub has not seen before asks for authorization. The hub knows who you are but will not hand a code to an unconfirmed client.
If you did not initiate the request, deny it.
A client keeps asking for the password
An approval is bound to the client ID and the redirect URI it was approved for. A client that changes its redirect target counts as new. Loopback URIs (http://127.0.0.1:<port>/…) match regardless of port, so a desktop client picking a fresh port each time is not the cause.
Check mcp-hub-admin clients list — if the same client name appears many times with different IDs, the client is re-registering instead of persisting its credentials.
/health returns 401
By design since v0.3.0: /health exposes per-server state and sits behind a bearer token. Point liveness monitoring at /livez, which is unauthenticated and returns {"status":"ok"}.
Since 0.5.0 the token also has to be the one for /hub — /health lists every server, so a token bound to a single server does not cover it.
fail2ban banned my own clients
A generic "ban on repeated 401" jail reading the reverse proxy's access log will do this: the MCP authorization flow produces legitimate 401 responses on every new connection. Exclude the hub's vhost from such jails, and use the purpose-built mcp-hub-auth filter, which matches the hub's own log lines instead.
Runtime
Tool calls fail after about a minute
Your reverse proxy is cutting the request. MCP tool calls are long-running; the hub allows 310 seconds and its internal call timeout is 5 minutes (reset on progress notifications). Raise proxy_read_timeout / the equivalent to at least 330 seconds.
Requests get 429 with MCP request rate limit exceeded
The per-client gate: MCP_REQUESTS_PER_MINUTE (default 120) and MCP_MAX_CONCURRENT_REQUESTS (default 4), counted per OAuth client. Raise them if a legitimate client needs more.
Editing mcp.json does nothing
The hub watches the file's directory and polls the file every 3 seconds, so a host-side edit should be picked up within a few seconds — the poller exists precisely because single-file bind mounts produce no inotify events in the container.
If nothing happens, look for ignoring broken config update: … in the log: an edit that does not parse is rejected and the previous configuration stays active.
Notifications from a server never arrive
The transport is stateless, so server-initiated messages — listChanged, resource subscriptions, sampling — are not delivered to clients. This is a deliberate trade for not leaking session state on every client reconnect. Tool, resource and prompt request/response traffic is unaffected.
A server name is rejected
Names must match [a-zA-Z0-9_-]+, and these are reserved because the hub serves them itself: mcp, hub, authorize, token, register, login, consent, health, livez, revoke, .well-known.
Operating
How do I update?
docker compose pull && docker compose up -dPin a version tag rather than latest so this happens when you decide. Read the changelog first — 0.3.0 moved /health behind authentication, which breaks external monitors that polled it.
Can several people use one hub?
Not meaningfully. There is one password and no user accounts; anyone who knows it can approve a client and reach every server. For roles, audit trails or SSO, use something built for that — see Comparison.
Can I hide a server from /hub but keep it reachable?
Yes: "hub": false. Its own path keeps working. This is the usual setup for servers you register as their own connector anyway, so they are not offered twice.
Is a mixed setup possible — some servers isolated?
That is the recommended shape when trust levels differ. Run the sensitive servers in their own containers and add them to the hub as type: "http" entries. Clients see one endpoint either way, and a remote upstream has no access to the hub's process or files.
Where do I report a security issue?
GitHub private vulnerability reporting. Please do not open a public issue, and leave real credentials and hostnames out of the report.