KDCube
← Recipes
KDCube Recipes · The Job Card

Expose a Governed Service over MCP: Your Tools, the User's Accounts, No Tokens in Your Code

One KDCube app exposes its functionality over a governed MCP door; its tools act on a user's accounts and your own OAuth-protected server, while Connection Hub authorizes every call and your code never touches a token.

19 July 2026RecipesGoverned MCPThe Job Card
MCPConnection Hubdelegated credentialsconnected accountsgoverned MCP serviceclaim layeringmanaged authper-account consent

WHAT LEAVES THE SHOP

One KDCube app exposes its own functionality over an MCP door; its tools act on a user's Gmail account and on your own OAuth-protected server; Connection Hub authorizes every call against the caller's delegated grant, resolves the account credential only at the trusted boundary, and your code never touches a token or a session. A resident agent and an external app (Claude Code) both call the same governed door.

You want functionality that reaches a user's third-party accounts and your own protected server, exposed over MCP — and you want the platform, not your code, to own the governance. This recipe builds exactly that: the minimal chain first, then the full setup, then the one thing that quietly breaks a governed service — where each claim has to live.

Current code and descriptors still say bundle in names such as bundle_id, bundles.yaml, and @bundle_entrypoint. In this recipe, app = bundle: one deployable KDCube runtime unit.

THE JOB CARD · GOVERNED MCP YOUR APP my-service@1-0 CONNECTION HUB connection-hub@1-0 domain service holds no token of its own acts on a passed-in credential @mcp door managed auth · the door body admits on named_services:use the door fronts your service body MANAGED GUARD authorizes every call resource · operation claims · identity · expiry provider connections delegated_to_kdcube which accounts + the claims they grant delegable resources delegated_credentials.oauth which doors may be delegated, to whom the governance store connection cards · delegation edges the live grant + its authority chain ONE CALL consults THE TRUSTED BOUNDARY passes account credential resolves the broker resolves it only here gmail:send on account A your service makes the call Gmail / my_server API the real third-party call NO TOKENS IN YOUR CODE · THE GUARD PASSES FIRST
Your app tools (which hold no credentials) on the left; Connection Hub's connections, delegable resources, and governance store on the right — one call passes the managed guard before a credential resolves.

Who owns what

who.owns.whatMAP
YOUR APP  my-service@1-0                      CONNECTION HUB  connection-hub@1-0
-----------------------------                 -----------------------------------
services/  domain logic (Gmail calls,         providers          which accounts a
           your-server calls) - no auth in it  (delegated_to_kdcube)  user may connect
surfaces/  @mcp door (managed auth)                                + the claims each grants
tools/     per-operation account claims       delegable          which doors may be
           ("this op needs gmail:send")        resources          delegated, and to whom
                     |                          (delegated_credentials.oauth)
                     |  a call arrives         THE GOVERNANCE STORE
                     v                         connection cards   one per (user, caller,
             Connection Hub authorizes  <----                     resource): the live grant
             by the caller's grant, then      delegation edges    the authority chain each
             resolves the account credential                      grant was minted through
             only at the trusted boundary

Your app declares the door and the required consents. Connection Hub is the authority: it captures the connection cards (the per-caller grants a user approves) and the delegation edges (the chain each grant was minted through), and it resolves the real provider credential only after a call passes. Your tool code sees an authorized request for a resolved user — never a credential.

The minimal chain (read this first)

The smallest working shape — one third-party (Gmail), one door, one consumer:

the.minimal.chainMAP
(1) connection-hub@1-0 . connections.delegated_to_kdcube.providers.google      -> the Gmail app + claims
(2) connection-hub@1-0 . connections.delegated_credentials.oauth.resources     -> make your door delegable
(3) my-service@1-0     . surfaces.as_provider.mcp.<door>.auth (managed)        -> the guarded door
(4) my-service@1-0     . tool code: connected_accounts [{provider_id, claims, claims_by_operation}]
(5) my-agent@1-0       . surfaces.as_consumer ... tools[] (delegated: true)    -> the consumer
(6) runtime            . user connects Gmail + grants the caller (per account)

Everything below fills in these six and adds your own OAuth service as a second provider. The full map, with every descriptor path, is The Connected-Services Config Chain.

YOU WILL NEED
  • a running KDCube tenant/project;
  • an app package to expose, here called my-service@1-0;
  • connection-hub@1-0 deployed (it owns the governance);
  • a Google OAuth client (client id + secret) for the Gmail connection;
  • your own OAuth/OIDC service's client credentials and endpoints, for the second provider;
  • an authenticated KDCube user who may delegate the grants;
  • optionally, an external MCP client (Claude Code) to prove the external path.

OP 10OF 70 Configure the Gmail provider and its claims

Edit: connection-hub@1-0config.connections.delegated_to_kdcube.

Owner: Connection Hub. It owns which providers a user may connect and the claims each connection can grant.

Gmail uses the built-in google.oauth adapter, so you supply only the OAuth client and the claims:

connection-hub@1-0 · delegated_to_kdcubeYAML
# connection-hub@1-0 · config.connections.delegated_to_kdcube
delegated_to_kdcube:
  enabled: true
  oauth: { public_base_url: "https://<host>" }   # where Google redirects back
  providers:
    google:
      adapter: google.oauth
      enabled: true
      connector_apps:
        gmail:
          client_id: "…apps.googleusercontent.com"
          client_secret_ref: "connections.delegated_to_kdcube.providers.google.connector_apps.gmail.client_secret"
          allowed_claims: [gmail:read, gmail:send]        # the ceiling for this app
      claims:
        gmail:read: { label: Read Gmail, provider_scopes: [openid, email, profile, "https://www.googleapis.com/auth/gmail.readonly"] }
        gmail:send: { label: Send Gmail, provider_scopes: [openid, email, profile, "https://www.googleapis.com/auth/gmail.send"] }
  • allowed_claims is the per-provider ceiling — the most a connected Gmail account may ever grant here.
  • claims.*.provider_scopes maps each KDCube claim to the Google OAuth scopes requested at connect time.
  • The secret is a *_ref pointer into bundles.secrets.yaml, never inline:
bundles.secrets.yamlYAML
# bundles.secrets.yaml · items[id=connection-hub@1-0].secrets
connections.delegated_to_kdcube.providers.google.connector_apps.gmail.client_secret: "<google-client-secret>"

Provider/account setup in depth: Delegated Provider Accounts.

CHECK · THE INSPECTOR'S STAMP

Apply and reload the config, open Connection Hub → Delegated to KDCube; Google appears as connectable, and a user can OAuth a Gmail account and approve gmail:read / gmail:send.

OP 20OF 70 Configure your own external OAuth/OIDC service

Edit: connection-hub@1-0config.connections.delegated_to_kdcube.providers.

Owner: Connection Hub. Your server is just a second provider.

Unlike Google, your server has no built-in adapter, so you register it as a custom OAuth/OIDC provider. The full walkthrough (authorize/token/userinfo endpoints, claim mapping, the adapter contract) is Custom OAuth/OIDC Provider Accounts; the shape mirrors Step 1:

connection-hub@1-0 · providersYAML
# connection-hub@1-0 · config.connections.delegated_to_kdcube.providers
    my_server:
      adapter: custom.oauth          # the OIDC/OAuth adapter — see the custom-oauth recipe
      enabled: true
      connector_apps:
        default:
          client_id: "<your-client-id>"
          client_secret_ref: "connections.delegated_to_kdcube.providers.my_server.connector_apps.default.client_secret"
          redirect_uri: "https://<host>/api/…/delegated_to_kdcube_oauth_callback"
          allowed_claims: [my_server:read, my_server:write]
      claims:
        my_server:read:  { label: "Read your server",  provider_scopes: [read] }
        my_server:write: { label: "Write your server", provider_scopes: [write] }

A single provider declares its own real claimsmy_server:read / my_server:write are the exact permissions your server enforces at its API, not an invented wrapper. (More on why in The claim layering below.)

CHECK · THE INSPECTOR'S STAMP

A user can now connect both a Gmail account and a "my_server" account under Delegated to KDCube, each approving its own claims.

OP 30OF 70 Write the app: domain service + MCP surface + required consents

Edit: the my-service@1-0 source — services/, surfaces/mcp/, entrypoint.py.

Owner: the app source. There is no governance here — only domain logic that holds no credentials, an MCP door, and a declaration of what each operation needs.

Keep the app modular. Put the third-party calls in a domain service that holds no credentials (it receives a resolved credential per call; it never stores or fetches one), expose them through an MCP surface, and declare, per operation, which connected-account claim it needs.

my-service@1-0/LAYOUT
my-service@1-0/
  entrypoint.py                 thin composition root; declares the @mcp door
  services/
    gmail_ops.py                domain logic that calls Gmail with a passed-in token
    my_server_ops.py            domain logic that calls your server with a passed-in token
  surfaces/
    mcp/service.py              the MCP tool definitions (the door body)
  config/ interface/ docs/ tests/
entrypoint.pyPYTHON
@mcp(alias="ops", route="public", transport="streamable-http",
     auth_config="surfaces.as_provider.mcp.ops.auth")
def ops_mcp(self, request=None, **kwargs):
    return build_ops_mcp_app(request=request, ...)

Declare what each operation needs from the connected accounts. This is the machine-readable source the proactive consent picker reads, so a user can consent before the agent ever calls:

connected_accountsPYTHON
OPS_CONNECTED_ACCOUNT_REQUIREMENTS = [
  { "provider_id": "google", "connector_app_id": "gmail",
    "claims": ["gmail:read", "gmail:send"],
    "claims_by_operation": { "object.search": ["gmail:read"], "object.action.send": ["gmail:send"] } },
  { "provider_id": "my_server", "connector_app_id": "default",
    "claims": ["my_server:read", "my_server:write"],
    "claims_by_operation": { "object.search": ["my_server:read"], "object.action.push": ["my_server:write"] } },
]

Read/write is the per-account provider claim declared here — not a door scope. The door admits on named_services:use; the per-account claim (resolved by Connection Hub's broker) is the read/write gate. Keep it here, once.

CHECK · THE INSPECTOR'S STAMP

The door route resolves at /api/integrations/bundles/<t>/<p>/my-service@1-0/public/mcp/ops, and the per-operation requirements surface in the chat consent picker before any call.

OP 40OF 70 Guard the door and make it delegable

Edit: two descriptors — my-service@1-0 (the guard) and connection-hub@1-0 (the delegable resource).

Owner: the app declares what authority it accepts; Connection Hub is that authority.

First, guard the door with managed auth in your app:

my-service@1-0 · surfaces.as_provider.mcpYAML
# my-service@1-0 · config.surfaces.as_provider.mcp
mcp:
  ops:
    auth: { mode: managed, authority_id: delegated_client, selected_tool_grants: true }

mode: managed hands authorization to Connection Hub — it checks every call against the caller's grant (resource, operation, claims, identity, expiry) before your tool runs. delegated_client is an authority id, not an app id; Connection Hub registers it when its OAuth block is enabled.

Second, declare your door as a delegable resource in Connection Hub, and (for external callers) whitelist their client:

connection-hub@1-0 · delegated_credentials.oauthYAML
# connection-hub@1-0 · config.connections.delegated_credentials.oauth
oauth:
  public_clients:                                  # external apps (Claude Code) allowed to connect
    - { client_id: "<external-app>", redirect_uris: [...] }
  resources:
    - resource: "*/api/integrations/bundles/*/*/my-service@1-0/public/mcp/ops*"
      label: "My service MCP"
      tools: { … per-operation grants … }          # named_services:use + the op's real claim

The resource pattern must byte-match the door URL and the consumer's resource in Step 5. Door mechanics and the managed guard in depth: Expose an MCP Service from a KDCube App and Protect App MCP with Managed Credentials.

CHECK · THE INSPECTOR'S STAMP

Call the door with no grant — the managed guard refuses and the consent card rises in chat; with a grant, the same call runs.

The claim layering — where every claim lives, and which are real

This is the part that makes or breaks a governed service, and it is easy to get wrong: one claim (say gmail:send or slack:search) appears in up to four places, and they must agree.

one.claim.four.layersMAP
one claim, four layers (all must agree)

  L1  grant vocabulary    connection-hub . oauth.capabilities                 DEFINES the claim (label, who may delegate it)
  L2  access map          connection-hub . oauth.resources[].named_services   which OPERATION requires it (per-call enforcement)
  L3  connection scope    consumer . as_consumer.agents.<a>.tools[].scopes    the agent's CEILING at this door
  L4  per-account bind     account_scope on the grant (runtime, in the Hub)    which account + which of its claims this caller may use
LayerWhere (descriptor path)Declares
1 · Grant vocabularyconnection-hub@1-0 · …oauth.capabilities — the - grant: <claim> entries with label, delegable_roles, delegable_permissionsDEFINES each delegatable claim. A claim used anywhere else MUST exist here, or it has no label and cannot be delegated.
2 · Access mapconnection-hub@1-0 · …oauth.resources[].named_services.<ns>.tools.<op>.grantsWhich grants each OPERATION requires — the door's per-call enforcement: named_services:use (door entry) plus the operation's specific claim.
3 · Connection scope<consumer> · surfaces.as_consumer.agents.<a>.tools[].scopesThe agent's CEILING — the most it may be granted at this door.
4 · Per-account bindingaccount_scope on the grant (runtime — Connection Hub UI, no descriptor)Which connected account, and which of its real claims, this caller may use.

Layers 1 and 2 are authored in the connection-hub config; the access map is a projection of it, invented nowhere else. This is why editing only one place (an agent's scopes, say) does not change what a demand asks for — the operation's requirement lives in the access map.

Which claims are REAL — the rule that avoids invented "wrapper" claims

The claims in layers 1–2 must be the real permissions the underlying service needs at its API, not an invented namespace wrapper:

  • A single provider — Gmail, Slack, your own OAuth service — declares its own real claims. Slack uses slack:search, slack:post, slack:channels, slack:history, slack:files:read, slack:files:write, slack:assistant:search (the exact Slack capabilities), never an invented slack:read / slack:write. These are the SAME tokens the connected account authorizes at layer 4, so all four layers speak one vocabulary. Your own server is the same: my_server:read / my_server:write are its real permissions.
  • A multi-provider realm — mail is the example: it serves Gmail (OAuth) AND IMAP/SMTP (username/password, which has no OAuth scopes at all). There is no single real claim across providers, so mail keeps a provider-agnostic namespace claim mail:read / mail:send at layers 1–2, with the real provider claim (gmail:read / gmail:send) resolved per account at layer 4.

So: real provider claims for a single-provider service; a provider-agnostic namespace claim only when the realm genuinely spans providers with no shared vocabulary (mail).

Account-backed claims never sit in a connection scope. For Gmail, Slack, or your own server, the read/write is the real provider claim, consented per account in Connection Hub — layer 4, not layer 3. The connection scopes (layer 3) carry just named_services:use plus any internal namespace claim (like conv:read) that has no per-account layer.

The built-in apps implement exactly this — read them as the worked reference:

  • …/examples/bundles/kdcube-services@1-0/ — the named-services door + provider code (mail, slack, conv, tasks) with claims_by_operation.
  • …/examples/bundles/connection-hub@1-0/config/bundles.template.yaml — the grant vocabulary (oauth.capabilities) and the access map (oauth.resources[].named_services), showing mail (provider-agnostic mail:read) beside slack (real slack:search / slack:post / …).

OP 50OF 70 Consume it

Edit: the consumer — either your agent's descriptor, or nothing at all for an external app.

Owner: the consumer app (for a resident agent); Connection Hub's public_clients (for an external app).

A resident agent declares a delegated connection to the door:

my-agent@1-0 · surfaces.as_consumerYAML
# my-agent@1-0 · config.surfaces.as_consumer.agents.<agent>.tools[]
- name: ops
  kind: mcp
  delegated: true
  url: "https://<host>/api/integrations/bundles/<t>/<p>/my-service@1-0/public/mcp/ops"
  resource: "*/api/integrations/bundles/*/*/my-service@1-0/public/mcp/ops*"
  scopes: [named_services:use]        # account-backed claims consent per account, not here

The two fields differ on purpose: url is the concrete endpoint dialed; resource is the exact delegated-resource id from Step 4, under which the grant is stored and looked up. They must byte-match the Step 4 pattern.

An external app (Claude Code) needs no consumer block — it connects to the same door over OAuth via the public_clients entry from Step 4. Consumer detail: Connect an MCP Service to a KDCube Agent.

CHECK · THE INSPECTOR'S STAMP

The resident agent lists the ops tools only after a grant exists; with no grant the connection stays unbound and KDCube never contacts the door.

OP 60OF 70 Runtime: the user connects and grants

Use: the running Connection Hub UI — no descriptor here.

Owner: the user. Two independent consents, in the Hub.

the.two.consentsTEXT
Delegated to KDCube      "KDCube may use my Gmail / my_server account, with these claims"
Delegated by KDCube      "this caller (agent or external app) may call this resource"
  1. Connect accounts (Delegated to KDCube): OAuth a Gmail account and a "my_server" account, approving each provider's claims.
  2. Grant the caller (Delegated by KDCube): the first call raises a consent card; the user grants per account, per claim (account_scope) — e.g. Gmail read+send via account A, read-only via account B.

These are the two gates every account-backed call passes, in order: the provider gate (the account's own scope, Delegated to KDCube) and the agent gate (this caller's per-account binding, Delegated by KDCube). A call whose account holds the claim but whose caller is not yet bound to use it there is refused with a distinct grant-this-agent ask — not a reconnect — and the connect step hands straight off to it ("Continue → grant it to this agent"). The full model is Claim-Driven Consent.

At this moment Connection Hub writes a connection card for (user, this caller, this resource) and records the delegation edge (the authority chain the grant was minted through). Both are inspectable and revocable in the Hub; the card is the live authority the guard resolves on every subsequent call.

the.call.pathFLOW
caller (resident agent OR external app)
  | KDCube delegated grant (bearer)
  v
managed door guard  -- resource + operation + claims + identity + expiry
  | passes
  v
your tool code  -- an authorized request for a resolved user (NO token)
  | asks the broker for THIS user's account credential
  v
Connection Hub broker  -- resolves the connected-account credential at the trusted boundary
  | gmail:send on account A
  v
Gmail API
CHECK · THE INSPECTOR'S STAMP

Bind read-only on one Gmail account and read+send on another; the send is refused on the read-only account, naming the allowed one — even though that account is itself send-capable. The binding decides, not the account.

OP 70OF 70 Verify every boundary

Use: the deployed URLs, the Connection Hub UI, and proc/audit logs — not the Python methods alone; that would bypass the descriptor and managed-guard joins this recipe exists to prove.

  • Publish — Gmail and "my_server" appear under Delegated to KDCube; a user can connect each.
  • No grant — a call with no grant raises the consent card; with a grant it runs; revoking the card stops it on the next call.
  • Per-account — bind read-only on one Gmail account, read+send on another; the send is refused on the read-only one, naming the allowed account.
  • Wrong resource — a token minted for another resource fails before server contact.
  • Layer agreement — remove the op's claim from the access map (L2); the demand stops asking for it even though the account still holds it. Restore it.
  • External path — connect the door from Claude Code via public_clients; the same guard authorizes it, and it never receives the Gmail token.
  • No credentials leak — grep prompts, generated code, logs, and the executor environment for the Google / my_server secret values; they must be absent.
FINAL INSPECTION · DONE MEANS
  • The app exposes its functionality over a managed MCP door; app code holds no token and no session.
  • Gmail and your own OAuth server are both connectable under Delegated to KDCube, each with its real claims.
  • Each operation declares its required per-account claim in connected_accounts / claims_by_operation — once, in code.
  • The door is guarded by mode: managed and registered as a delegable Connection Hub resource whose pattern byte-matches the URL and the consumer's resource.
  • Every claim agrees across its four layers; account-backed claims live at layer 4 (per account), never in a connection scope.
  • Single-provider services use their real claims; only a genuinely multi-provider realm (mail) uses a provider-agnostic claim.
  • A resident agent and an external app both call the same door; each gets its own grant, and a grant to one is authority for no other caller.
  • Per-account binding is enforced: a write is refused on a read-only-bound account even when that account is write-capable.
  • Wrong resource, missing grant, and provider/grant revocation all fail closed; no secret appears in prompts, code, logs, or the executor.

Build it in depth

Read the architecture and position

Sibling recipes

KDCube Recipe
№ 2026-07-19 · kdcube.tech