An external client speaks MCP — tools/list, tools/call over HTTP — and wants to reach a KDCube service. How does it authenticate?

The lazy answer is a shared secret: paste an API key into the connector, and any caller holding that string is "the integration." That answer fails the moment you ask the real questions. Whose data is this acting on? What may it do? Who pays? How do you turn one connection off without breaking the rest?

KDCube's answer is a delegated credential: a bearer that KDCube issues, scopes to one resource, narrows to the tools and grants a user consented to, and records back to that user. It is categorically different from a shared secret, and the difference is the subject of this piece.

This is the protocol layer the rest of the Connection Hub series pointed at. Delegating A KDCube Service To An External App (#2) told the connect → consent story from the user's side; Protecting KDCube Surfaces With Managed Credentials (#3) covered the guard that enforces it. This piece owns the wire: MCP as the transport, OAuth as the client-facing adapter, and the delegated credential as the authorization model that ties them together. It builds on the identity model (#1) and the guard (#3) by reference.

MCP            is the transport surface.       (what the client speaks)
OAuth          is the client-facing adapter.   (how the client gets a credential)
delegated      is the authorization model.     (what the bearer actually is)
  credential

Three layers, one connection

The three words above are not synonyms, and keeping them apart is the whole design. Each layer has one job and a different owner.

ONE CONNECTION — THREE DISTINCT LAYERS MCP the transport surface — what the client speaks tools/list · tools/call · stateless HTTP owner bundle · proc OAuth the client-facing adapter — how the client obtains a credential discovery · authorize · token · PKCE owner Connection Hub (AS) Delegated credential the authorization model — what the bearer actually is scoped recorded revocable per-user owner Connection Hub + managed guard
Three distinct jobs — transport, adapter, authorization model — not three names for one thing.

MCP — the transport surface. The external client speaks the Model Context Protocol: initialize, tools/list, tools/call, over streamable HTTP. In KDCube there is no platform-level /mcp; MCP is exposed by individual bundles and served by proc. A bundle declares the surface and points its handler at a descriptor:

@mcp(
    alias="memories",
    route="public",
    transport="streamable-http",
    auth_config="surfaces.as_provider.mcp.memories.auth",
)
async def memories_mcp(self, request: Request, **kwargs):
    return build_memory_mcp_app(...)

The handler does not parse tokens, know the client's id, or check grants. It receives a request that already passed the guard (#3).

OAuth — the client-facing adapter. "OAuth" is how the client obtains a credential, not the authorization model itself. KDCube is the OAuth2 authorization server for this flow — it does not hand the integration step to an external IdP. The adapter exists because clients like Claude already speak OAuth; KDCube meets them on that protocol and hands back a credential.

Delegated credential — the authorization model. The bearer the client ends up holding is a Connection Hub delegated credential: a scoped, recorded, per-user, revocable grant. This is the layer that makes the connection safe, and the rest of this article is mostly about it.

The authenticated-MCP handshake

Here is the actual end-to-end sequence. The example resource is the user-memories@2026-06-26 memories MCP.

THE AUTHENTICATED-MCP HANDSHAKE EXTERNAL CLIENT KDCUBE OAUTH + MCP BROWSER / USER 1 tools/call · no credential 401 WWW-Authenticate: Bearer resource_metadata 2 GET .well-known/oauth-protected-resource + -authorization-server 3 POST /oauth/register (DCR allowlist) 4 GET /oauth/authorize (PKCE S256) 5 browser login + consent the ONLY step where the user's own credentials appear — the external client never sees them 6 redirect: code + state + iss 7 POST /oauth/token (code + PKCE verifier) kst1 delegated credential + refresh 8 tools/call + Bearer managed guard verifies product result every later call rides the credential from step 7 — checked against the stored consent on each request
Step 5 is the only place the user's own credentials appear; every later call rides the credential minted at step 7.
1. DISCOVER
   Client POSTs the bundle MCP URL with no valid credential.
   Proc MCP bridge replies 401 with an RFC 9728 challenge:
     WWW-Authenticate: Bearer resource_metadata="<.../oauth/.well-known/oauth-protected-resource?resource=...>"

2. READ METADATA
   Client fetches the protected-resource doc (RFC 9728) and the
   authorization-server doc (RFC 8414). It learns:
     authorization_endpoint = .../connection-hub@1-0/public/oauth/authorize
     token_endpoint         = .../connection-hub@1-0/public/oauth/token
     registration_endpoint  = .../connection-hub@1-0/public/oauth/register
     resource               = the concrete bundle MCP URL
     scopes + tool catalog (+ named-service catalog, if any)

3. REGISTER (if needed)
   Client POSTs /oauth/register with its redirect URIs.
   KDCube accepts only descriptor-allowlisted redirect URIs (DCR allowlist).

4. AUTHORIZE
   Browser opens /oauth/authorize?response_type=code&code_challenge=<PKCE S256>
     &scope=<grant>&client_id=<public client>&redirect_uri=<validated>
   Connection Hub validates the existing platform session cookie (KDCube login),
   then renders the consent screen.

5. CONSENT
   The signed-in user approves selected tools + the platform delegation edge.
   The consent POST is CSRF-protected and re-validates client/redirect/PKCE.

6. CODE
   Redirect back to the client with code + state + iss.

7. TOKEN
   Client POSTs /oauth/token: grant_type=authorization_code, code, code_verifier.
   KDCube verifies code, client, redirect URI, and PKCE; mints a short-lived
   kst1 integration session; stores the refresh token, selected-tool allowlist,
   credential envelope, and the delegation edge(s).
   Client now holds:  access_token (least-privilege) + refresh_token (rotating).

8. CALL
   Client POSTs the bundle MCP URL:
     Authorization: Bearer <access_token>
     JSON-RPC tools/list or tools/call
   The managed guard verifies the bearer against the stored consent (#3),
   then product code runs.

Two things to notice. First, the user authenticates in a normal browser session at step 4 — the external client never sees those credentials; it only ever holds the token from step 7. Second, the flow uses public clients with PKCE and needs no new secret in secrets.yaml. There is no shared secret to hand out in the first place.

What the bearer actually is

The token the client presents is a kst1 session token for a derived integration identity — not the user's session, and not an opaque shared key. Its meaning lives in a server-side record bound to a kdcube.credential.v1 envelope, so product code never decodes grantor facts from the token body:

{
  "schema": "kdcube.credential.v1",
  "credential_kind": "delegated_client_access",
  "issuer_authority_id": "delegated_client",
  "issuer_authenticator_id": "delegated_client.bearer",
  "subject": "integration:claude:<grantor-sub>",
  "audience": "kdcube:delegated_client",
  "attrs": {
    "client_id": "claude",
    "resource": ".../user-memories@2026-06-26/public/mcp/memories",
    "scopes": ["memories:read"],
    "tools": ["memory_search", "memory_get"],
    "identity_scope": "grantor_identity_family"
  }
}

The subject is a derived identity — integration:claude:<grantor-sub> — minted from the consenting user's subject, never the user's own id. The external client gets its own actor identity (#1); the consenting user remains the grantor. The token carries the resource it is bound to, the tools and scopes consented, and the identity scope — and the durable delegation edge, selected-tool allowlist, and grantor authority facts sit in the grant store, refreshed on rotation without re-consent.

Why this is not a shared secret

This is the title's claim, made concrete. A shared API secret and a delegated credential look superficially alike — both arrive as Authorization: Bearer … — but they answer the hard questions in opposite ways.

The same questions, answered by a shared secret versus a delegated credential.
Question Shared secret Delegated credential
Who is acting? nobody in particular — whoever holds the string a derived actor, integration:claude:<grantor-sub>, recorded
On whose behalf? unknown the consenting grantor, on the stored edge
What may it do? whatever the endpoint allows the key only the consented tools, only if their grants are present
Which resource? often any that accepts the key exactly the one URL the credential is bound to
Who pays? untraceable the projected platform subject (#1)
Turn one off? rotate the key — breaks every holder revoke one credential record; others keep working
What leaks if stolen? a long-lived, broad secret a short-lived, single-resource, least-privilege token

A shared secret is an authentication fact with no authorization model behind it. A delegated credential is the inverse: the bearer is almost incidental; the record is where authority, scope, provenance, and revocation live. That is why losing the Redis grant record fails closed rather than open — a credential with no record behind it authorizes nothing.

It is worth naming what this is explicitly not: not a replacement for browser login, not a way to mint platform-admin sessions, not a broad "admin API" token. It is a descriptor-configured adapter from an already-authenticated consent flow to a least-privilege credential.

The MCP runtime is stateless on purpose

KDCube's proc-served MCP endpoints build stateless FastMCP apps:

mcp = FastMCP(
    "KDCube Memories",
    stateless_http=True,
    instructions="Call named_services_list first ...",
    icons=[...],
    website_url="https://...",
)

The reason is the distributed runtime. A request flows ingress → proc → bundle MCP app, and may be handled by a different worker or a fresh app instance each time. FastMCP's stateful streamable-HTTP mode keeps mcp-session-id transports in local Python memory — not a durable boundary when the next request lands elsewhere. KDCube keeps durable state where it belongs instead:

credential + consent state   -> Connection Hub grant store
identity edges + projection  -> Connection Hub store / runtime
product state                -> product store
MCP protocol exchange        -> stateless handler, nothing to lose on reconnect

So reconnecting a client never depends on a Python object still being alive in one worker. (If stateful MCP protocol features are ever needed, the right owner is a platform MCP ingress/broker, not a bundle-local object — that direction is noted in #3.)

The worked example: user memories

The focused example is user-memories@2026-06-26, whose MCP surface exposes memory_search and memory_get. The interesting part is what happens after the guard admits the call — identity scope:

Claude calls memory_search (Bearer)
      |
      v
managed guard verifies credential / resource / tool / grants     (#3)
      |
      v
identity_scope on the credential = grantor_identity_family
      |
      v
memory service asks Connection Hub: delegated_identity_scope_resolve
      |
      v
Connection Hub returns memory_user_ids = [grantor, linked Telegram user, ...]
      |
      v
memory store query runs over exactly those owner ids

A memory can be created from a browser session, from Telegram, or by a delegated client; each record keeps its real owner for provenance. When the resource opts into grantor_identity_family, the product resolves the connected family server-side — the client never sends user ids. The delegated client does not become the user's browser session; it is a delegate actor operating through a selected edge and selected grants. (The resolution mechanics are #1; the guard that gates it is #3.)

The generic named_services bridge — one connector reaching mem, task, and cnv namespaces through a second, nested grant boundary — is the same model applied more broadly. That is its own piece; see Named services can now leave KDCube through a delegated MCP connector.

What the client sees, and what is actually enforced

A good MCP server helps the external model use the surface: server instructions, tool names and descriptions, MCP ToolAnnotations, a server icon, a website URL, and the protected-resource / dynamic-registration metadata from discovery. KDCube advertises all of these.

But none of it is the security boundary. ToolAnnotations such as readOnlyHint and destructiveHint let a client group tools in its UI — read-only apart from write/action — and that is purely connector UX. The enforcement is always the managed guard checking the call against the stored credential. Presentation and authorization are related surfaces; they are not the same boundary.

client UX            -> instructions, tool annotations, icon, website_url
                        (helps the model; not a gate)

security boundary    -> managed guard: resource, tool, grants, consent  (#3)
                        (the only gate)

Where it is configured

Two descriptor locations, two owners — the same split #2 and #3 use. The bundle descriptor declares the surface; Connection Hub declares what is delegable.

# bundle: what the app provides
surfaces:
  as_provider:
    mcp:
      memories:
        auth:
          mode: managed
          authority_id: delegated_client
          tools:
            memory_search: { grants: [memories:read] }
            memory_get:    { grants: [memories:read] }
          selected_tool_grants: true
# connection-hub@1-0: what can be delegated, and the OAuth protocol config
connections:
  delegated_credentials:
    oauth:
      enabled: true
      public_clients:
        - client_id: claude
          redirect_uris:
            - https://claude.ai/api/mcp/auth_callback   # illustrative
            - http://localhost/callback
      capabilities:
        - grant: memories:read
          label: Read memories
          delegable_roles: [kdcube:role:chat-user, kdcube:role:paid, kdcube:role:super-admin]
      resources:
        - resource: "*/user-memories@2026-06-26/public/mcp/memories*"
          identity_scope: grantor_identity_family
          tools:
            memory_search: { label: Search memories, grants: [memories:read] }
            memory_get:    { label: Read one memory,  grants: [memories:read] }

mode: managed is what makes the proc MCP bridge own the credential and grant check before dispatching into the bundle app; without it, the auth block is bundle-owned metadata. The OAuth protocol config — public_clients, dynamic_client_registration.allowed_redirect_uris, issuer, enabled — lives only on the Connection Hub bundle, and toggling enabled: false disables the whole public OAuth operation.

Related publications

Connected Identities Are Not One User Id — why a delegated client stays its own actor while the grantor is reached through an explicit edge; the model behind identity_scope and economics attribution.
Delegating A KDCube Service To An External App — the user-facing connect → consent journey that produces the credential this protocol issues.
Protecting KDCube Surfaces With Managed Credentials — the managed guard that verifies the bearer on every call, in order, before product code runs.
Named services can now leave KDCube through a delegated MCP connector — the generic named_services bridge as a specific external-client case of this same authenticated-MCP model.
KDCube Deep · 30.06.2026