Skip to content

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

URLUse it for
https://mcp.example.net/hubone connector for everything, four meta-tools, minimal context cost
https://mcp.example.net/<name>/mcpa 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

sh
claude mcp add -t http paperless https://mcp.example.net/paperless/mcp
claude mcp add -t http hub https://mcp.example.net/hub

Claude 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/mcp gets 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

Decision flow when a client requests authorizationGET /authorizeValid login session?noyesPassword page10 tries / 15 min per IPClient alreadyapproved?noyesCorrect passwordcounts as consentApprove / DenyCSRF-protected page
A client is confirmed exactly once, bound to its client ID and the redirect URI it used.

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 code10 minutes, single use
Access token15 minutes, self-contained EdDSA-signed JWT
Refresh token30 days, rotated on every use
Login session cookie30 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.

Released under the MIT License. Not affiliated with Anthropic; “Claude” is a trademark of Anthropic PBC.