HTTP endpoints
Everything mcp-hub serves, and what guards it.
MCP endpoints
| Path | Auth | Purpose |
|---|---|---|
/<name> | Bearer | Streamable HTTP endpoint of one server (canonicalized to /<name>/mcp) |
/<name>/mcp | Bearer | same, canonical form |
/hub | Bearer | aggregate endpoint exposing the six meta-tools |
<name> is a key from mcpServers. Both routes accept the HTTP methods the Streamable HTTP transport uses.
A server that is not up answers 503 with a JSON-RPC error naming its state:
{
"jsonrpc": "2.0",
"error": { "code": -32000, "message": "Server \"paperless\" is starting" },
"id": null
}Status
| Path | Auth | Purpose |
|---|---|---|
/livez | none | process liveness; always 200 {"status":"ok"} while the process runs |
/health | Bearer for /hub | per-server state; 200 when every server is up or sleeping, 503 otherwise |
/health reports the same fleet-wide view as the /hub aggregate, so it takes the same resource: a token issued for one server's path gets 401 here.
/livez is what the image HEALTHCHECK calls, and it is the endpoint external monitoring should poll. A degraded child does not make the container unhealthy.
/health response:
{
"status": "degraded",
"servers": {
"paperless": { "state": "up", "kind": "stdio", "restarts": 0, "tools": 14, "inFlight": 1, "hub": true },
"internal-only": { "state": "down", "kind": "remote", "restarts": 2, "tools": 0, "inFlight": 0, "hub": false },
"scraper": { "state": "up", "kind": "docker", "restarts": 0, "tools": 84, "inFlight": 0, "hub": true,
"image": "scraper-mcp@sha256:…", "container": "mcp-sandbox-scraper" }
}
}state is one of starting, up, down, sleeping, unauthorized. kind is stdio, remote, docker or socket. restarts counts supervisor restarts since boot. inFlight counts the requests forwarded to the server whose answer the hub is still waiting for; a call its client cancelled stops counting at once. hub says whether the server appears in the /hub aggregate. A sleeping on-demand server counts as healthy — it is resting by design, and its cached tools count stays visible. unauthorized is a remote server whose upstream OAuth needs attention; it is counted as degraded and, unlike down, is not retried until somebody acts. A sandboxed server also reports the image and container it runs as — a local tag and a name, not credentials, and the difference between "scraper is down" and something you can act on.
A server with allowTools or denyTools also carries a toolFilter object — exposed, hidden and any unmatched entries. It is absent for every server without a filter, and tools keeps its meaning: the number a client can see. hidden and unmatched appear only once the server has really listed its tools: a sleeping server restored from the tool cache holds a snapshot that was already filtered, so there is nothing honest to say about them yet, and the two keys stay away rather than reporting a zero.
OAuth 2.1
| Path | Auth | Purpose |
|---|---|---|
/authorize | session / password | authorization endpoint (PKCE required); accepts an https URL as client_id |
/token | client secret, private_key_jwt or none | token and refresh endpoint |
/register | none | dynamic client registration (RFC 7591); 404 when disabled |
/register/<client_id> | registration_access_token | the client's own registration (RFC 7592): GET to read, PUT to change, DELETE to remove it; 404 when dynamic registration is disabled |
/upstream/callback | signed state + hub session | where an upstream sends the browser back after an upstream login; single use |
/.well-known/mcp-hub-client/<id>.json | none | the hub's own client metadata document, one per oauth.mode: "cimd" upstream; 404 for an identifier nobody publishes |
/revoke | client credentials | token revocation (RFC 7009) |
/interaction/<uid>/ | — | the page an unfinished authorization sends the browser to: the password form, or the Approve / Deny page for a signed-in operator and a client not yet confirmed |
/interaction/<uid>/login | password | a correct password approves the requesting client; 503 while no usable password is configured |
/interaction/<uid>/consent | session + CSRF token | Approve / Deny for a client not yet confirmed |
Auth responses carry Cache-Control: no-store. The interactive pages deny framing and carry a restrictive CSP.
A client_id that is an https URL with a path is treated as a Client ID Metadata Document and fetched from the client; anything else is looked up as a dynamically registered client. Which mechanisms are accepted is set with CLIENT_REGISTRATION.
Rate limits are listed on the security page.
Discovery documents
| Path | Auth | Purpose |
|---|---|---|
/.well-known/oauth-authorization-server | none | RFC 8414 authorization-server metadata |
/.well-known/oauth-authorization-server/<suffix> | none | same document, path-inserted form |
/.well-known/oauth-protected-resource | none | RFC 9728 protected-resource metadata |
/.well-known/oauth-protected-resource/<suffix> | none | path-scoped variant |
/.well-known/openid-configuration | none | alias serving the same RFC 8414 document |
/.well-known/openid-configuration/<suffix> | none | same alias, path-inserted form |
The authorization-server document advertises both registration mechanisms:
{
"client_id_metadata_document_supported": true,
"registration_endpoint": "https://mcp.example.net/register",
"token_endpoint_auth_methods_supported": ["client_secret_post", "none", "private_key_jwt"],
"token_endpoint_auth_signing_alg_values_supported": ["RS256", "…", "EdDSA"]
}client_id_metadata_document_supported is what makes a spec-compliant client prefer CIMD; registration_endpoint is absent when dynamic registration is turned off, and the CIMD fields are absent when metadata documents are.
The hub is not an OpenID Connect provider — there is no userinfo endpoint and no id_token. The alias exists because several clients, ChatGPT among them, probe the OIDC path before the RFC 8414 one, and the document answers every field they read there. Enabling a client's OIDC option on top of it does not work.
The path-scoped resource document is what makes resource-bound tokens work without client configuration. A client connecting to /paperless/mcp looks up /.well-known/oauth-protected-resource/paperless/mcp and gets:
{
"resource": "https://mcp.example.net/paperless/mcp",
"authorization_servers": ["https://mcp.example.net"],
"bearer_methods_supported": ["header"],
"resource_name": "mcp-hub"
}It then requests a token for exactly that resource. One authorization server covers every resource the hub exposes.
Reserved names
These cannot be used as server names, because the hub serves them itself:
mcp · hub · authorize · token · register · login · consent · health · livez · revoke · jwks · interaction · session · userinfo · upstream · .well-known · unlock
The check is case-insensitive and happens when the config is parsed.