Your MCP handler should never see an unauthorized call. By the time memory_search runs, the request has already been checked: the credential is real, it was issued for this resource, this tool was consented, and the tool's required grants are present. If any of those fails, the caller gets a denial and your product code is never invoked.

That gate is the subject of this piece. Delegating A KDCube Service To An External App covered what the user connects and consents to; this one covers what the guard actually checks at every call, in order, before your handler sees the request. It is the enforcement side of the same story, and it is the developer's view: descriptors, mode: managed, resource matching, tool grants, and where identity scope fits.

bundle declares:   I provide this surface. These tools exist. Each needs these grants.
Connection Hub holds:  the issued credential + the consent edge (resource, tools, grants, scope).
the surface guard does:  let this call into product code, or fail it closed.

The result is a clean boundary: product code is not a token parser. This builds on the vocabulary from the two prior pieces — actor, grant, identity family (#1) and the delegated credential + consent edge (#2) — and assumes them rather than re-deriving them.

Two modes: bundle-owned vs managed

A surface declares one of two auth modes, and they can coexist on the same bundle.

The two auth modes a surface can declare — and when each is right.
Mode Who owns auth Use when
mode: bundle the bundle owns its own auth contract (e.g. a private header token) a bundle intentionally has a private integration contract — local and explicit
mode: managed Connection Hub owns delegation, issuance, verification, consent, and enforcement a KDCube service should be delegated to external apps through platform-owned consent and least privilege

One bundle can do both:

surfaces:
  as_provider:
    mcp:
      knowledge:          # private contract
        route: operations
        auth:
          mode: bundle
          header_name: X-Knowledge-MCP-Token
      memories:           # delegated, platform-managed
        route: public
        auth:
          mode: managed
          authority_id: delegated_client

The goal is not to abolish bundle-owned auth. It is to make delegated external access a platform-managed surface, not a bundle-local invention.

What the surface declares

The security boundary lives in descriptors, so it is visible before any code runs and admin tooling can reason about what a bundle exposes. The provider bundle marks the surface managed and maps each tool to the grants it requires:

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

Three pieces matter here, and the guard reads each one:

  • authority (authority_id) — which credential authority is accepted at this boundary (delegated_client);
  • tool policy (tools) — which tools exist and which grants each requires;
  • selected_tool_grants — whether a tool must also have been individually consented, not just be grant-eligible (defaults to true).

That descriptor shape is exactly what the guard parses into its policy object — authority_id, optional endpoint roles/permissions, per-tool grants, and selected_tool_grants. The handler, by contrast, should be boring:

@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(...)

It does not parse the credential. It receives a request that has already passed the managed guard for the surface. (One runtime note, expanded in the MCP piece: the proc-served FastMCP app should be built stateless_http=True, because the durable credential and consent state lives in Connection Hub, not in bundle-local protocol memory.)

What Connection Hub declares

The provider descriptor says what the surface needs. Connection Hub declares what is delegable and how grants map to concrete resources — the same two-row shape from Delegating A KDCube Service To An External App:

connections:
  delegated_credentials:
    oauth:
      enabled: true
      capabilities:
        - grant: "memories:read"
          label: "Read memories"
          delegable_roles:
            - "kdcube:role:chat-user"
            - "kdcube:role:paid"
            - "kdcube:role:privileged"
            - "kdcube:role:super-admin"
      resources:
        - resource: "*/api/integrations/bundles/*/*/user-memories@2026-06-26/public/mcp/memories*"
          identity_scope: "grantor_identity_family"
          tools:
            memory_search:
              grants: ["memories:read"]
            memory_get:
              grants: ["memories:read"]

The capability row answers who may delegate this grant (delegable_roles) and what label the consent screen shows. The resource row answers which concrete resource this is, which tools it exposes, which grants each needs, and which identity scope it opts into. The split matters because one grant can appear on several resources, and one resource exposes several tools. Both blocks are the real shapes from the shipped user-memories@2026-06-26 and connection-hub@1-0 descriptors.

Note where identity_scope lives: on the resource row, not on the tool. It is not a per-call gate the guard enforces — it is a setting the product reads after the guard passes (more below).

The guard pipeline, in order

This is the load-bearing section. When a managed surface receives a request, the proc bridge runs the guard before dispatching to the bundle handler; if the guard returns a denial, the handler is never called. Here is the exact order the guard checks:

THE GUARD PIPELINE — EVERY GATE FAILS CLOSED Bearer · tools/call incoming request mode: managed? no → skip guard (bundle owns auth) Bearer token present? no → 401 unauthorized token authenticates? no → 401 unauthorized endpoint roles / permissions? no → 403 forbidden load grant record + credential envelope authority match? no → 403 forbidden resource match — exact? no → 403 forbidden tools/call? (initialize / tools/list accept here) no tool call → accept per tools/call, in order 1 · tool in endpoint policy? 2 · tool roles / permissions? 3 · tool grants ⊆ credential grants? 4 · tool consented? (selected_tool_grants) any fails → per-call tool error other calls in the batch continue product code runs reached only after every gate passes
Product code sits at the bottom, behind every gate; each gate has a fail-closed exit, and nothing reaches the handler unauthorized.
0. Is this surface mode: managed?
     no  -> skip the guard entirely (bundle owns its own auth)
     yes -> continue

1. Bearer token present?
     no  -> 401 unauthorized  (+ WWW-Authenticate pointing at OAuth metadata)

2. Token authenticates as a delegated-client session?
     no  -> 401 unauthorized

3. Endpoint-level roles/permissions satisfied? (if the descriptor sets any)
     no  -> 403 forbidden

4. Credential authority == descriptor authority_id?
     no  -> 403 forbidden  (authority mismatch)

5. Credential resource present AND == this request's resource?
     no  -> 403 forbidden  (resource missing / resource mismatch)

   --- at this point a non-tools/call request (initialize, tools/list) is accepted ---

6. For each tools/call in the body:
     a. tool is in the endpoint tool policy?      no -> tool error: not allowed
     b. tool roles/permissions satisfied?         no -> tool error: missing role/permission
     c. tool's required grants ⊆ credential grants? no -> tool error: missing grant
     d. selected_tool_grants on, and tool ∈ consented tools? no -> tool error: not consented

7. All checks pass -> attach runtime authority context -> product handler runs.

Two details worth calling out.

Resource matching is exact, not prefix. Both sides are normalized — query string dropped, trailing slash stripped — and then compared for equality. A credential minted for one MCP URL does not work on another. (The wildcard * patterns you saw in the descriptors match resources to capability rows at consent time; the per-request guard check is an exact string match against the credential's stored resource.)

Tool checks are layered and all fail closed. A tool can be rejected for four distinct reasons — not in the endpoint policy, missing a role/permission, missing a grant, or never consented — and the guard returns a per-call JSON-RPC error for each, leaving other calls in the batch unaffected.

Why grant, tool, and consent are separate

The guard keeps three questions apart on purpose, and a credential can pass one while failing another.

authority  -> "which credential kind is accepted here?"   delegated_client
grant      -> "what permission was delegated?"             memories:read
tool       -> "what action is being attempted?"            memory_get
consent    -> "did the user actually select this tool?"    selected_tool_grants

So a credential with memories:read does not automatically authorize every memories:read tool. If the user consented to memory_search only, a memory_get call fails closed even though its grant is present:

tools/call:                memory_get
descriptor requires:       grants = [memories:read]
credential grants:         [memories:read]          (grant check: passes)
consented tools:           [memory_search]          (consent check: fails)

result:  "tool not consented for this connection: memory_get"  -> fail closed

That is what gives consent real teeth: approving a grant is not approving every tool it could unlock. And if a tool needs two grants — say knowledge:read and knowledge:maintain — it declares both, and a credential with only one fails the grant check.

Identity scope comes after the guard

Authorization is not the same as data selection. The guard decides whether the call may enter product code. It does not resolve which user ids the product should read — that is a separate step the product performs after the guard, and only if it needs cross-identity aggregation:

WHAT YOU DECLARE → WHAT THE GUARD ENFORCES DESCRIPTOR DECLARES GUARD GATE authority_id: delegated_client which credential kind is accepted resource: …/memories* which concrete resource this is memory_get → memories:read which grants each tool requires selected_tool_grants: true tool must also be consented identity_scope: grantor_identity_family not a gate — read after the guard authority match else 403 resource match — exact else 403 grant gate tool grants ⊆ credential grants consent gate tool ∈ consented tools product code resolves identity scope after admission the gate Every declared line becomes one gate — except identity_scope, which the product resolves past the guard.
Every declared line becomes one gate — except identity_scope, which the product resolves past the guard, in product code.
guard accepts the call
        |
        v
product handler runs
        |
        | (only if it aggregates across identities)
        v
asks Connection Hub: delegated_identity_scope_resolve
        |
        v
Connection Hub returns memory_user_ids = [grantor, linked Telegram user, ...]
        |
        v
product reads by exactly those owner ids

The external app never sends memory_user_ids. The backend gets them from Connection Hub, bounded by the resource's identity_scope and the grantor's connection edges. The mechanics of that resolution are the identity model's job — see Connected Identities Are Not One User Id. The guard's runtime projection simply carries the accepted credential's identity_scope forward so the product can ask later.

Generic bridges have a second, nested boundary

One managed surface can be a generic bridge over many namespaces. kdcube-services@1-0/public/mcp/named_services is the example: its tools are generic (named_services_search, named_services_get, …) and the namespace is a tool argument.

That creates two boundaries, checked by two owners:

outer  (managed guard):       the generic MCP tool + named_services:use
inner  (named-service bridge): per-namespace grant, e.g.
                                 mem.search   -> memories:read
                                 mem.upsert   -> memories:write
                                 task.search  -> tasks:read

The outer guard from the pipeline above admits the generic tool. The named-service bridge then checks the namespace operation's grant against the same credential before calling the provider; if it is missing, the tool returns a delegated_consent_required result naming the missing grant. This is why the server advertises instructions to call named_services_list first: the connector starts generic, discovers namespaces, then uses each namespace's capability/schema before calling it.

What this enables

Once a bundle can declare managed surfaces, KDCube can host a real connection experience without each bundle reinventing auth:

Connect an external app to KDCube memories.
Connect an external app to KDCube knowledge.
Connect a partner agent to one narrow support tool.
Connect an internal automation to one bundle API.

Each connection then carries a clear service label, tool-level consent, grant-level enforcement, identity-scope control, revocation, economics projection, and actor provenance — every one of them declared in descriptors and enforced by one shared guard. That is the difference between "a token happens to work" and "a platform can safely host many user-delegated services."

Related publications

Delegating A KDCube Service To An External App — the user-facing connect → consent flow that produces the credential and consent edge this guard enforces.
Connected Identities Are Not One User Id — the identity and authority model behind identity_scope, grantor/delegate edges, and economics attribution.
Authenticated MCP In KDCube: Delegated Credentials, Not Shared Secrets — the MCP transport behind this surface: OAuth discovery, the protected-resource challenge, stateless MCP surfaces, and tool annotations.
Named services can now leave KDCube through a delegated MCP connector — the generic managed MCP surface where one connector bridges several provider namespaces with namespace-specific grants.
KDCube Deep · 29.06.2026