Skip to content

Standards ​

What mcp-hub implements, in which direction, and where the limits are.

Two directions matter, and they are genuinely different problems. Inbound is mcp-hub as an authorization server and resource server for the MCP clients that connect to it. Outbound is mcp-hub as an OAuth client toward the remote MCP servers it connects to on your behalf. Most gateways do one of the two.

Getting a client identity ​

The same four ways in and out — with one gap in each direction, named below.

Inbound (a client → the hub)Outbound (the hub → an upstream)
Operator-issued credentialsmcp-hub-admin clients add issues a client_id and secret by handoauth.mode: "static" with a clientId the upstream gave you
Dynamic registration (RFC 7591)POST /register, on by defaultoauth.mode: "dcr"
Registration management (RFC 7592)GET/PUT/DELETE /register/<client_id>the hub deletes its own registration on upstream logout
Client ID Metadata Documentsa client uses its document URL as client_idoauth.mode: "cimd", one document per upstream
Bearer token, no OAuthAPI tokens minted by the operatora static Authorization header

CIMD is draft-ietf-oauth-client-id-metadata-document-00, the mechanism the MCP specification prefers over dynamic registration.

Proving that identity ​

InboundOutbound
client_secret_post✅✅
client_secret_basic❌ — credentials are read from the form body only✅
none (public client)✅✅
private_key_jwt (RFC 7523)✅ for metadata-document clients only✅ via oauth.clientAuth

Both directions sign and verify RS/PS/ES 256/384/512 and EdDSA. Outbound, the hub signs with an Ed25519 key of its own at <DATA_PATH>/upstream-key.pem — deliberately not the key that signs the access tokens it issues — and publishes the public half in the document or registration the upstream reads.

Grants ​

InboundOutbound
authorization_code + PKCE✅ S256 required, plain rejected✅ S256
refresh_token✅ rotating, with reuse detection✅ serialized, single-flight
client_credentials❌✅

Inbound refresh tokens rotate on every use and are grouped into families: replaying a retired token revokes the whole family, because a replay means two parties hold one chain. Outbound, refresh is deliberately taken away from the SDK and serialized per upstream — parallel requests that each hit a 401 would otherwise each spend the same rotating token, which an upstream that detects reuse treats exactly as above.

Discovery and binding ​

StandardInboundOutbound
RFC 8414 authorization server metadata✅ served, incl. the path-inserted form✅ consumed
RFC 9728 protected resource metadata✅ served, incl. path-scoped variants✅ consumed, to find the upstream's authorization server
RFC 8707 resource indicators✅ enforced by default✅ sent when the upstream publishes RFC 9728
RFC 8252 native apps✅ loopback redirects, port-flexiblen/a
RFC 7009 revocation⚠️ refresh tokens only — see below✅ best effort on upstream logout

Resource indicators are the reason a token issued for one server does not reach another. Every access token carries the resource as its audience, and every request is checked against the path it arrived on.

/.well-known/openid-configuration is served as a byte-identical alias of the RFC 8414 document, because several clients probe it first. That is all it is: there is no ID token, no userinfo, no OIDC claims. Do not enable a client's "OpenID Connect" option against it.

Not implemented ​

Named explicitly, so you do not have to find out by trying:

Revoking somebody else's tokens/revoke revokes the one access or refresh token the client presents (RFC 7009). To withdraw every token of a client at once, use mcp-hub-admin clients revoke, which sets a marker the verifier checks.
client_credentials inboundThe hub issues tokens to a person who approved a client, not to a machine identity. Use an API token for that.
Device authorization grant (RFC 8628)Neither direction.
private_key_jwt for dynamically registered clientsInbound it is accepted only from metadata-document clients. A DCR client uses its secret.
DPoP (RFC 9449), mTLS (RFC 8705), PAR (RFC 9126), token introspection (RFC 7662), token exchange (RFC 8693), client_secret_jwtNone of them, in either direction.
Verifying the hub's tokens elsewhereOAuth access tokens are opaque and only the hub's store can answer for them; API tokens are EdDSA-signed and verified by the hub alone. /jwks is served because oidc-provider serves it, and holds nothing a third party could verify a token against.
Scopes as an authorization boundaryScopes are carried through but nothing is enforced on them. Authorization is by resource, not by scope.
Users, roles, audit trailsOne shared password, no per-user identity. Every token's subject is the same. See what the hub does not protect against.
Per-user upstream tokensAn upstream credential belongs to the deployment, not to the client that triggered the call. The hub does not act on behalf of individual users.

Transport and protocol ​

Streamable HTTP, stateless — no session state to leak — with SSE accepted for upstreams that only speak that. Bearer tokens are read from the Authorization header only; there is no query-parameter or form-body form.

The hub speaks both MCP revisions on every endpoint, and the client picks:

RevisionHow it is chosenWhere
2026-07-28server/discover/hub, /<name>/mcp, mcp-hub-stdio
2025-11-25initializethe same three

Which one you are on is a wire detail. The rule the hub holds itself to is that a client cannot tell from the answers: same tools, same names, same endpoint.

What is carried, per revision ​

Every row here has a test, and the ones that say no say so because there is no test that could pass — not because the detail was left out of the table. The hub has twice announced something it did not deliver (listChanged, resources.subscribe), and both cost more to find than they would have cost to admit.

Traffic2026-07-282025-11-25
tools/list, tools/callcarriedcarried
resources/list, resources/templates/list, resources/readcarriedcarried
prompts/list, prompts/getcarriedcarried
completion/completecarriedcarried
A tool's outputSchema and a result's structuredContentcarriedcarried, with the SEP-2106 wrap
Elicitation — a child asking the person a questioncarried both waysnot offered
Embedded sampling/createMessage or roots/list in a child's questiondropped, and named in the log—
describe_connection, where MCP_DIAGNOSTICS is onanswers, and reports this eraanswers, and reports this era
subscriptions/listen — a child's changes reaching a clientcarriednot offered
listChanged notifications (tools, prompts, resources)delivered on a subscriptionnot advertised, not delivered
notifications/resources/updateddelivered on a subscriptionnot advertised, not delivered
resources/subscriberemoved from the revision; a child's subscribe capability is re-advertised so a client knows subscriptions/listen may name resource URIsnot advertised, refused
logging/setLevel, notifications/messagenot advertised, no handlernot advertised, no handler

Subscriptions are the second row worth reading twice, for the same reason as the first. subscriptions/listen is a long-lived stream, which sounds like the one thing a stateless gateway cannot hold — but the state is the open HTTP response, not a session table. When the socket goes, so does the subscription, and nothing is left behind to leak. That is why this could be built without giving up the property the rest of the design rests on. See Subscriptions.

To a 2025-11-25 client the hub offers none of it. That revision delivers changes unsolicited on a channel the stateless transport does not keep, and resources/subscribe needs the hub to remember who asked for what — so rather than announce a listChanged whose notification would never arrive, the hub stays quiet. The hub announced exactly that for a long time and it was a lie; the row above is what it says now.

logging is the third. logging/setLevel never had a handler on either era, so a client that believed the advertisement got a -32601 at call time. On 2026-07-28 the level is per-request _meta and there is no RPC left to implement; carrying notifications/message is separate work. Until it exists the capability is not offered.

The structured-output row has a footnote. 2026-07-28 lets a tool's outputSchema describe any JSON value (SEP-2106); 2025-11-25 allows only an object, so a child whose schema describes an array or a number reaches a 2025 client with both the schema and the value wrapped in {"result": …}. The hub does that wrapping where it is the one talking to the older client, and leaves it alone where the child already did it — a gateway that unwrapped somebody else's answer to make it look modern would be inventing a shape nobody sent. Object-rooted schemas, which is nearly all of them, are identical on both.

The hub's own meta-tools all use an object root for exactly this reason: with an array root a client could tell from the payload which revision it had been given, and the promise above is that it cannot.

Elicitation is the row worth reading twice. It is what smtp-mcp and imap-mcp use to put a question in front of a person before they send or delete something, and behind a gateway it used to have nowhere to go. On 2026-07-28 it does: the question is a result, the call ends, the person decides, and the client retries with the answer. Nothing is held open, which is exactly why a stateless hub can carry it. See Elicitation.

To a 2025-11-25 client over HTTP the hub does not offer it at all. That revision delivers a question as a server-initiated request, and a stateless hub has no channel for one — so rather than announce a capability whose answer would be dropped, the hub stays quiet and the child takes its own fallback. That is the same rule as the two rows above it, applied before the mistake instead of after.

The authorization behaviour follows the specification revision that made metadata documents the preferred registration mechanism and deprecated dynamic registration — which the hub still serves, because most clients still need it.

Next ​

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