Connecting clients
Every MCP endpoint the hub exposes is Streamable HTTP behind an OAuth 2.1 bearer token. Any client that supports remote MCP servers with OAuth can use it.
Which URL to register
| URL | Use it for |
|---|---|
https://mcp.example.net/hub | one connector for everything, four meta-tools, minimal context cost |
https://mcp.example.net/<name>/mcp | a single server with its native tools, for the ones you use constantly |
Both can be registered at the same time. A common setup is one /hub connector plus two or three direct connectors, with those servers marked "hub": false so they are not offered twice.
The short form https://mcp.example.net/<name> works as well and is canonicalized to /<name>/mcp.
Claude Web
Settings → Connectors → Add custom connector, then enter the URL.
Claude registers itself through dynamic client registration, opens the hub's login page, and asks for the password. Entering it correctly signs you in and approves that client in one step. From then on the connector reconnects on its own.
One login, several connectors
The login session lasts 30 minutes. Adding a second connector within that window shows an Approve / Deny page instead of the password prompt — the hub already knows who you are, but it will not hand a code to a client you have not confirmed.
Claude Code
claude mcp add -t http paperless https://mcp.example.net/paperless/mcp
claude mcp add -t http hub https://mcp.example.net/hubClaude Code opens a browser for the OAuth flow on first use, the same way.
Other clients
Anything that implements the MCP authorization spec works. The hub publishes the standard discovery documents, so a client only needs the endpoint URL:
/.well-known/oauth-protected-resource[/<path>]— RFC 9728, tells the client which authorization server to use, path-scoped so/paperless/mcpgets its own resource identifier/.well-known/oauth-authorization-server[/<path>]— RFC 8414 metadata for the hub's own authorization server
Dynamic client registration (/register) is open, as the MCP specification intends. Registration alone grants nothing: a client only receives an authorization code after you have confirmed it, and only at the redirect URI you confirmed.
The approval flow
An approval records the client ID together with the redirect URI it was approved for. A client that later asks for a different redirect target is treated as new and needs confirming again. Loopback redirect URIs — the http://127.0.0.1:<port>/… form desktop clients use — match regardless of port, as the OAuth specification requires.
Denying sends the client away with error=access_denied.
Token lifetimes
| Authorization code | 10 minutes, single use |
| Access token | 15 minutes, self-contained EdDSA-signed JWT |
| Refresh token | 30 days, rotated on every use |
| Login session cookie | 30 minutes, HttpOnly, SameSite=Lax, __Host- prefixed over HTTPS |
Refresh tokens rotate: each use issues a new one and retires the old. Replaying a token that was already rotated away revokes the whole chain, on the assumption that it leaked. A refresh can never widen scope beyond the original grant.
Resource-bound tokens
Every token is bound to the single resource the client asked for. A token issued for /paperless/mcp cannot call /hub, /health or any other server's path — /health counts as part of /hub, because it reports the same fleet-wide view.
Clients discover the right resource identifier from the RFC 9728 document of the endpoint they connect to, so this needs no client-side configuration. An authorization request that names no resource at all is refused with invalid_target.
Upgrading from 0.4 or earlier
Before 0.5.0 this had to be switched on with RESOURCE_BOUND_TOKENS=true, and tokens issued without a resource reached every path. Those unbound tokens stop working the moment binding is enforced, so every connector authorizes once more after the upgrade.
If you need to postpone that, set RESOURCE_BOUND_TOKENS=false. It keeps the old behaviour, logs a warning on every start, and is meant to be removed again — not to be left in place.
Revoking a client
Use the offline admin command; it is covered on the deployment page.
Notification support
The transport is stateless, which means server-initiated messages are not delivered to clients: listChanged notifications, resource subscriptions and sampling requests do not travel outward. Tool, resource and prompt request/response traffic works in full.
Internally the hub does follow tools/list_changed from its children, so the /hub tool cache stays current even though clients are not notified.