Operations
Shutdown cancels background connections and waits for retired configuration runtimes to finish draining before closing shared storage. Requests still follow the configured drain timeout.
- Use
/healthzfor process liveness and/readyzfor verifier/required-backend readiness. See HTTP endpoints. - Use SIGHUP for reloadable YAML fields. Listener, identity, administration, Vault/portal and other static settings require restart. Once initialized, managed storage supplies backend configuration; YAML backends no longer apply. See reload and shutdown.
- Back up the database with its matching
MCPHUB_CONFIG_KEY; coordinate Vault data backups when enabled. Recheck grants, accounts and configuration after recovery. Choose a database driver when creating the deployment. - Monitor recording gaps, failed approval deliveries and archive backlogs. Request history and recent configuration events do not replace independent approval-audit archives.
Docker deployment
The Dockerfile builds a static binary with golang:1.26.8-bookworm, then copies it into gcr.io/distroless/static-debian12:nonroot. The final image has no shell and runs as the nonroot user.
docker build -t mcphub:local .
docker run --rm \
--name mcphub \
-p 127.0.0.1:8080:8080 \
--env-file .env \
-v "$PWD/config.yaml:/etc/mcphub/config.yaml:ro" \
mcphub:local serve --config /etc/mcphub/config.yaml
The container listen address must match the published port (the example uses :8080). --env-file injects environment variables only; the configuration is mounted read-only. Restrict permissions on the host config.yaml and .env. The image entrypoint is already /usr/local/bin/mcphub, so pass serve or validate as arguments.
SQLite administration needs a writable database volume; both stores need MCPHUB_CONFIG_KEY. Local mode binds container loopback and is not reachable through ordinary port publishing. For remote container management use mode: remote, a built-in administrator account and an HTTPS proxy; see the PostgreSQL Compose guide.
HTTP endpoints and RFC 9728
Assuming server.public_url: https://hub.example.com/mcp:
| Address | Auth | Semantics |
|---|---|---|
GET /healthz |
none | Returns 200 {"status":"ok"} while a runtime exists; use it as a liveness probe. |
GET /readyz |
none | Returns 200 when the authentication verifier and all required backends are ready, otherwise 503. JSON includes backends_ready, backends_total, required_ready, required_total, and auth_verifier_ready. |
GET /.well-known/oauth-protected-resource/mcp |
none | Path-aware RFC 9728 Protected Resource Metadata address; expose this address to clients. |
GET /.well-known/oauth-protected-resource |
none | Root-path compatibility alias for the same metadata. Route both addresses to MCPHub through a reverse proxy. |
/mcp |
Bearer JWT | Stateless MCP Streamable HTTP entry; accepts POST only, and compatibility clients use the same /mcp POST semantics. Modern clients may use request-scoped SSE in the POST response; MCPHub does not provide a standalone GET SSE or DELETE session endpoint. Catalogs and calls are filtered by token scopes. |
Metadata has resource equal to the complete public_url, authorization_servers containing auth.issuer, scopes_supported equal to the union of backend required scopes, and bearer_methods_supported equal to header. An MCP request without a token receives a 401 challenge whose resource_metadata points to https://hub.example.com/.well-known/oauth-protected-resource/mcp; missing scopes return 403 insufficient_scope.
Cross-origin requests accept only the public_url origin or an exact origin in allowed_origins. Preflight responses allow only POST (with OPTIONS as the preflight response) and the implemented MCP/trace headers (including supported Mcp-Param-* parameter headers); * is not supported. Other paths return 404.
SIGHUP reload and shutdown
On Unix, send signals to a running serve process:
kill -HUP <mcphub-pid> # reload the same --config file
kill -TERM <mcphub-pid> # graceful shutdown
SIGHUP fully loads, expands, and validates the configuration before building a candidate runtime; failures leave the old runtime in place. Required backends in the new runtime must connect successfully on the initial attempt. The old runtime waits for active requests for drain_timeout before closing; when a runtime generation closes, it cancels request contexts bound to that generation before closing its Hub/backend state, preventing late session registration. That cancellation immediately expires the underlying write deadline, interrupting slow or unread subscription writes after the drain; ordinary requests retain their request_timeout deadline. Allowed origins, catalog/request/drain parameters, the backend list, backend authentication, scopes, and backend-local tool_rules can be reloaded. Changes to these fields are rejected and require a restart:
server.listenserver.public_url- all
authfields (includingauth.sso) - all
client_authorizationandvaultfields - every
adminfield
These values determine bound listeners, storage/encryption identity, RFC 9728/JWT audience, and the OIDC verifier, so they cannot be changed by replacing only the in-memory runtime. In admin mode SIGHUP reloads static YAML fields and composes backends from the selected database; YAML backend changes are ignored after first import.
SIGHUP candidate startup uses a cancelable context; shutdown cancels a candidate that is still connecting. Candidate required backends must connect successfully before the swap, and each candidate or retired runtime closes its own backend sessions and connections.
SIGINT and SIGTERM first stop MCPHub from accepting new requests, then keep the current runtime and backend context alive while HTTP requests drain for drain_timeout. If the HTTP drain reaches that timeout, MCPHub force-closes the remaining HTTP connections; generation close then cancels request contexts bound to the generation before backend state is closed.
When SIGHUP creates an unavailable optional backend, it inherits the previous in-memory catalog only when its catalog-source identity is unchanged: backend ID and URL, allow_insecure_http, every fixed header, the complete credentials configuration, and OAuth configuration presence plus type, issuer, client_id, client_secret, and scopes must match. Any credential, OAuth, or tenant-selection-header change blocks reuse. Fields that do not identify the catalog source, such as required, required_scopes, tool_rules, and timeouts, do not block reuse; inherited data never marks the new backend ready.