MCP in KDCube: Connect Any Agent, Expose Any App, Govern Every Call
How KDCube governs MCP in both directions: per-agent consumer inventories, app-native provider surfaces, delegated authority, dual-era wire support, named services, and economics around paid work.
MCP standardizes how an agent discovers and calls a tool. It does not decide which agent receives that tool, who may cross the endpoint, whose data the call can reach, or who pays for the work. KDCube keeps those decisions around the protocol:
as_consumerconnects a governed tool view to each agent;as_providerexposes an app-nativeKDCubeMCPServersurface; Connection Hub binds external actors to approving users and concrete grants; accounting and economics control paid work. Named services can add reusable object semantics, but they are not required to use MCP in either direction.
MCP made one part of agent integration pleasantly ordinary. An MCP agent can ask a server what tools it has, inspect their schemas, and call one without learning a vendor-specific RPC protocol. That is useful. It is also only the wire.
The production questions begin one layer out:
Which of my agents can see this server?
Which tools does each agent receive?
Where is the endpoint and how is its credential resolved?
Can an external client act for a user without becoming that user?
Does consent cover this exact tool and resource?
Which identity owns product data, and which identity pays?
What happens when the tool returns a file or a result too large for context?
This article maps the actual KDCube implementation that answers those questions. It covers both directions: consuming an MCP server from any hosted agent and exposing MCP from any KDCube app.
One terminology note: current source, descriptors, and routes still use
bundle in literal identifiers such as bundle_id, bundles.yaml,
@bundle_entrypoint, and /api/integrations/bundles/.... In builder-facing
prose, that deployable unit is an app.
01 The whole map: two directions, four owners
An app can consume MCP, provide MCP, or do both. The directions are independent.
KDCUBE RUNTIME
external MCP server external MCP client
| |
| transport + client credential | MCP + bearer
v v
+----------------------+ +----------------------+
| surfaces.as_consumer | | surfaces.as_provider |
| mcp.services | | mcp.<alias>.auth |
+----------+-----------+ +----------+-----------+
| |
v v
+----------------------+ +----------------------+
| per-agent inventory | | proc MCP boundary |
| allowed tools/traits | | public/app/managed |
+----------+-----------+ +----------+-----------+
| |
v v
KDCube-hosted agent app MCP tools
connection owner: app descriptor boundary owner: app descriptor
agent policy owner: app descriptor delegated grants: Connection Hub
remote price owner: explicit adapter paid work: accounting + economics
02 MCP 2026-07-28 changes the wire, not the authority model
The MCP 2026-07-28 specification
makes the protocol core stateless. A modern client can use
server/discover before a call or send a self-contained RPC directly;
the protocol no longer relies on the older
initialize/initialized session handshake. Application state can still
exist, but it has to be represented explicitly by the application instead of
being implied by a protocol session.
KDCube serves both eras from the same stateless app endpoint during migration:
modern client -> server/discover (optional) or a direct self-contained RPC
legacy client -> initialize -> tools/list -> tools/call
\-> same app tools and managed guard
The KDCube MCP client uses modern discovery first and falls back to legacy
initialize for an older peer. App code does not branch on the negotiated
version.
Client registration is a separate axis. Connection Hub resolves a public OAuth client in this order:
1. descriptor pre-registration
2. HTTPS Client ID Metadata Document (CIMD)
3. Dynamic Client Registration (DCR) compatibility
|
v
same PKCE -> consent -> grant -> refresh -> revocation path
Registration establishes the caller's identity and exact callback set. It grants no resource, tool, account, or provider authority. Those decisions stay in the managed grant and are checked again on every call. The MCP 2026-07-28 authorization specification defines the protocol-facing flow; KDCube binds it to its resource and consent model.
The official MCP Conformance Test Framework makes wire behavior independently testable. KDCube has direct dual-era regression coverage for its server, client fallback, and proc bridge. That is not presented here as a completed upstream conformance certification; a release claims full conformance only after it has run the official suite against the deployed endpoint.
Four owners keep the design understandable:
| Owner | Question it answers |
|---|---|
| Consumer server registry | Where is the server, which transport reaches it, and how is the client credential resolved? |
| Per-agent tool inventory | Which server and which concrete tools may this agent discover and call? |
| Provider auth policy + Connection Hub | Which external actor may cross this exact resource/tool boundary, on whose behalf, and with which grants? |
| Accounting + economics | What usage occurred, what did it cost, may it run, and which projected subject funds it? |
MCP does not replace any of these owners. It gives all of them a common tool transport to govern.
03 Direction 1: connect an MCP server to your MCP agent
The canonical consumer configuration deliberately separates one server connection from the views granted to individual agents.
Register the connection once
The app-wide connection lives under
surfaces.as_consumer.mcp.services.mcpServers:
surfaces:
as_consumer:
mcp:
services:
mcpServers:
docs:
transport: streamable-http
url: https://mcp.example.com/docs
auth:
type: bearer
secret: b:mcp.docs.token
That block owns endpoint, transport, and credential reference. Supported client
transports are stdio, http/streamable-http, and sse. A URL must be
reachable from the runtime process that owns the MCP client, not merely from the
developer's browser. In a container, 127.0.0.1 means that container.
The secret reference is resolved at runtime. The token is not copied into agent configuration, a model prompt, or a tool call. Tool-list cache keys include a hash of resolved auth headers so catalogs that legitimately differ by principal do not collide; the credential itself is not the cache key or log payload.
Attach a narrowed view to each agent
The agent connection lives separately:
surfaces:
as_consumer:
default_agent: main
agents:
main:
tools:
- kind: mcp
server_id: docs
alias: docs
allowed: [search, fetch]
tool_traits:
search:
strategy: [exploration]
fetch:
strategy: [exploration]
The model-facing ids become:
mcp.docs.search
mcp.docs.fetch
server_id selects the connection. alias selects the model-facing tool
namespace. allowed is the administrator's tool allow-list for that agent.
tool_traits describes planner/execution strategy; it is not authorization and
does not widen the allow-list.
Two agents can use one server without receiving equal authority:
agents:
analyst:
tools:
- kind: mcp
server_id: crm
alias: crm
allowed: [search_customers, read_customer]
operator:
tools:
- kind: mcp
server_id: crm
alias: crm
allowed: [search_customers, read_customer, update_customer]
one physical CRM MCP connection
|
+-> analyst: search + read
`-> operator: search + read + update
This is the first governance boundary: registering a server does not expose it to every agent, and attaching it to one agent does not attach it to another.
04 The consumer package journey
For the KDCube ReAct agent, the descriptor path is direct:
effective app props
surfaces.as_consumer.mcp.services
surfaces.as_consumer.agents.<agent_id>.tools
|
v
agent_tool_config_from_bundle_props(...)
|
+-> mcp_tool_specs
| {server_id, alias, tools}
+-> allowed_tool_names_by_alias
+-> tool_traits / runtime policy
|
v
BaseWorkflow.build_react(
mcp_tools_spec=...,
tool_traits=...,
)
|
v
MCPToolsSubsystem
connection registry + secret resolution + tools/list
|
v
filtered catalog: mcp.<alias>.<tool>
|
v
agent emits complete tool call
|
v
io_tools.tool_call -> MCPToolsSubsystem.execute_tool -> MCP transport
The parser creates the policy half:
AgentToolConfig(
mcp_tool_specs=[
{"server_id": "docs", "alias": "docs", "tools": ["search", "fetch"]}
],
allowed_tool_names_by_alias={"docs": ["search", "fetch"]},
# tool traits, runtime policy, and other resolved fields omitted
)
The MCP subsystem combines that policy with the separate server registry. The model receives only the resulting agent-scoped catalog.
This is not ReAct-only
A hosted LangGraph, CrewAI, Claude Agent SDK, or custom loop needs the same two inputs:
agent policy
AgentToolConfig.mcp_tool_specs
= server refs + aliases + allowed tool names
connection registry
surfaces.as_consumer.mcp.services
= transports + endpoints + credential references
Its adapter translates the selected server entries into that framework's MCP
client, discovers tools, filters them to mcp_tool_specs, and binds only that
narrowed list to the agent. The app descriptor stays the authority; framework
code should not carry a second URL list, a second token store, or a second
hand-written allow-list.
The KDCube LangGraph reference demonstrates the framework seam through the shared SDK v2 adapter: resolve the selected server map, load MCP tools asynchronously, and bind them alongside plain LangChain tools. The canonical production configuration remains the two-part registry + per-agent policy above; the framework adapter does not become another configuration owner.
For Claude Code as a subprocess, .mcp.json is the client contract. The KDCube
Claude workspace adapter can generate it from selected app configuration before
the process starts. Changing surfaces.as_consumer does not mutate an already
running external CLI process.
05 Trusted routing, untrusted execution
The boundary remains intact when an agent uses isolated code execution:
model-generated code
|
v
network-isolated executor
no MCP credential
|
| approved tool request over supervisor channel
v
trusted supervisor
MCP registry + secret resolution + transport
|
v
MCP server
The executor does not receive the full app configuration or MCP token. It asks the trusted supervisor to execute an allowed tool. That preserves the same tool inventory while keeping credentials out of unsafe code.
06 Direction 2: expose MCP from any KDCube app
The provider path is equally small. Build a KDCubeMCPServer and expose it
through one @mcp method.
from kdcube_ai_app.apps.chat.sdk.runtime.mcp.server import KDCubeMCPServer
from kdcube_ai_app.infra.plugin.bundle_loader import mcp
def build_reports_mcp(service):
server = KDCubeMCPServer("Reports", stateless_http=True)
@server.tool(
name="search_reports",
description="Search reports visible to the authorized principal.",
)
async def search_reports(query: str, limit: int = 10):
return await service.search(query=query, limit=min(max(limit, 1), 25))
return server
class ReportingApp(...):
@mcp(
alias="reports",
route="public",
transport="streamable-http",
auth_config="surfaces.as_provider.mcp.reports.auth",
)
async def reports_mcp(self, request=None, **kwargs):
return build_reports_mcp(self.reports.for_request(request))
The route is:
/api/integrations/bundles/<tenant>/<project>/<bundle_id>/public/mcp/reports
or, for route="operations":
/api/integrations/bundles/<tenant>/<project>/<bundle_id>/mcp/reports
The surface uses streamable HTTP. KDCubeMCPServer defaults to
stateless_http=True; the explicit argument makes the distributed-serving
contract visible. It is not cosmetic: proc dispatch is request-by-request, and
the next MCP request may hit another worker or a fresh app instance. Durable
credential, consent, product, and job state live in their stores. An MCP server
object in one worker is not a durable boundary.
App tools remain ordinary async product code. The official MCP SDK supplies
protocol discovery and schemas; KDCubeMCPServer adds the defaults used by the
distributed proc bridge, including modern and legacy wire acceptance. KDCube
supplies loading, routing, request context, descriptor policy, secrets,
observability, and optional managed authorization and economics.
07 Three provider authorization modes
The provider descriptor selects who owns authorization:
| Mode | Shape | Meaning |
|---|---|---|
| Intentionally public | no managed/app-owned mode | no credential guard; suitable only for genuinely public, bounded work |
| App-owned | auth.mode: bundle |
the app validates its own credential and applies its own policy |
| Platform-managed | auth.mode: managed |
KDCube/Connection Hub validates delegated credential, resource, grants, and selected tool before app code runs |
The route and auth mode answer different questions. A public/mcp/... route is
reachable for MCP discovery and OAuth challenge. It can still require a managed
bearer for every useful call.
The managed provider declaration is compact:
surfaces:
as_provider:
mcp:
reports:
auth:
mode: managed
authority_id: delegated_client
selected_tool_grants: true
Connection Hub owns the delegable capability and the concrete resource/tool catalog:
connections:
delegated_credentials:
oauth:
capabilities:
- grant: reports:read
label: Read reports
delegable_roles:
- kdcube:role:registered
- kdcube:role:paid
resources:
- resource: >-
*/api/integrations/bundles/*/*/reporting@1-0/public/mcp/reports*
tools:
search_reports:
label: Search reports
grants: [reports:read]
get_report:
label: Read report
grants: [reports:read]
The app says, "this endpoint uses managed delegated-client auth." Connection Hub says, "these tools exist, these grants unlock them, and these roles may delegate those grants." Keeping those owners separate lets one authority govern many app surfaces without moving product code into the authority service.
08 What happens before a managed tool runs
The proc MCP bridge resolves the app and its effective descriptor before it
calls the MCP builder. For mode: managed, the request body is inspected and
the Connection Hub guard runs first:
POST .../public/mcp/reports
|
v
resolve app + @mcp endpoint + effective auth descriptor
|
v
mcp_auth_mode == managed ?
|
v
authorize_delegated_mcp_request
1. bearer present and valid
2. issuer authority matches
3. credential resource matches this public URL
4. resource grants cover this tool policy
5. concrete tool is selected in the consent/grant record
|
+-- deny: HTTP/MCP error; app tool body never runs
|
v
apply delegated runtime projection
|
v
bind request/app-operation contexts
|
v
invoke app @mcp method -> MCP dispatch -> async domain service
tools/list is protocol discovery; concrete tools/call is where per-tool
grant and selected-tool enforcement applies. MCP ToolAnnotations help a client
present read/write/destructive intent. They do not grant authority. The
Connection Hub record and product checks do.
Account-backed tools add a second call-time decision below that managed
door. When several connected accounts qualify, the operation returns
account_required with labeled candidates instead of choosing one.
The caller's selected account_id then reaches both requirement
preflight and the provider operation. If the call names an account outside the
caller's binding, the client-facing denial is
agent_account_binding_required; its recovery URL identifies the
existing caller card, protected KDCube resource, account, and claim. The host
may present that path, but KDCube does not open the Hub, edit the grant, or
replay the operation.
09 MCP, OAuth, and delegated credentials are different layers
These terms often get collapsed, which makes the system sound more mysterious than it is:
MCP tool discovery and invocation transport
OAuth client-facing adapter used to obtain a credential
delegated credential stored authorization model behind that bearer
Connection Hub owner of consent, grants, identity projection, revocation
An external client first probes the MCP resource. The managed guard returns a protected-resource challenge. The client follows Connection Hub metadata, the user signs in through the normal KDCube authority, approves the concrete resource/tools/grants, and the client receives a short-lived delegated bearer.
On later calls the bearer identifies a derived external actor, not the user's browser session. The durable grant record retains what matters:
delegate actor
approving grantor
concrete resource
resource grants
selected tools
identity scope
expiry and revocation
Revoking one connection removes that authority without rotating a shared key or breaking unrelated clients.
10 Actor and grantor survive the boundary
After acceptance, the runtime projection deliberately preserves two identities:
delegate_identity / actor_identity
the external client that made this request
grantor_user_id / platform_user_id
the KDCube user who approved access and whose scoped product data is used
economics_user_id
the projected platform subject used for funding/economics decisions
The app therefore does not need to accept user_id as a tool argument. It reads
the request's identity_authority, uses platform_user_id for product scope,
keeps delegate_identity in provenance/audit records, and derives economics from
economics_user_id.
This projection also carries grants, selected operations, identity scope, and
authority provenance. Product storage still performs domain authorization: a
valid reports:read grant does not make an arbitrary report id visible if the
grantor cannot read that record.
11 Named services are optional, not a toll booth
MCP and named services solve different problems.
Use an app-native KDCubeMCPServer when product-specific tools are the
clearest contract:
search_reports(query, limit)
get_report(report_id)
create_forecast(request_id, assumptions)
Use a named-service provider when multiple apps, agents, widgets, and scenes should share a typed object realm with stable refs and a common grammar:
namespace: report
refs: report:item:<id>
operations:
provider.about
provider.capabilities
object.schema
object.search
object.get
object.upsert
object.action
The generic kdcube-services@1-0 MCP surface can project configured named
services to external agents. That gives an agent one discovery/search/get/action
grammar across memory, conversation, canvas, mail, or another provider. It also
supports nested namespace grants behind the outer MCP resource grant.
For a manual automation bearer, Connection Hub renders those existing namespace
operations as checkboxes. Selecting mem.object.search sends the exact
named_service_operations[resource][namespace][] choice and stores a narrowed
copy of the descriptor's named_services policy in the server-side grant
record. The outer managed guard still checks the generic MCP tool and
named_services:use; the inner bridge rejects every unselected namespace and
operation.
That nested selection is live server-side:
delegated_access_update can replace it without reminting the bearer.
The current existing-card editor preserves the selection but does not yet
render the namespace picker, so this particular in-place change is currently
available through the update operation.
A provider-backed namespace can also display its Slack, Gmail, or provider-account requirements. Those are completed under Delegated to KDCube and remain separate from the automation's KDCube delegation. The external agent never receives the provider token.
For hosted agents, the named-services MCP connection asks only for
named_services:use. The bridge derives the selected operation from the
catalog and rebinds the live card's account_scope at each tool-call entry.
A matching provider claim on that account binding satisfies the operation gate
without duplication in outer resource grants; the provider broker then enforces
the account-specific claim before making the upstream call.
But an app does not need to become a named-service provider to expose MCP, and an agent does not need named-service tools to consume an MCP server. Named services add reusable object semantics. MCP remains the transport.
12 Results and files: keep bytes out of model context
An MCP result normally becomes a structured tool-result block. Good result contracts are bounded and actionable:
- cap and paginate searches;
- return stable ids or object refs;
- return summaries before full bodies;
- use explicit error shapes;
- make write operations idempotent with caller-supplied request ids;
- return short-lived download URLs or product refs for large/binary files.
KDCube's hosted MCP client normalizes the protocol boundary before the model sees it. If MCP v2 returns one JSON text content block, the client unwraps it to the direct success or error object. Timeline steps retain compact result facts such as status, error code, item count, candidates, and recovery action instead of exposing a giant escaped transport envelope.
Do not return a large binary as base64 merely because JSON can carry it. That inflates the payload and puts bytes into the agent's context.
There is one narrower KDCube path: an MCP-backed tool executing inside the
current ReAct workspace can declare files with
ret.artifact_type == "files" when those paths really exist under the current
turn_<id>/files/... output tree. The runtime then hosts the declared files.
A remote MCP service cannot write into that workspace; it should return an
already-hosted reference or URL instead.
13 Economics: MCP does not imply free, paid, or metered
MCP carries a call. It does not declare a price.
Keep three mechanisms separate:
| Mechanism | Question |
|---|---|
| Accounting/tracking | What service and units were used? |
| Pricing | What did those units cost? |
| Economics enforcement | May this flow run, which funding source pays, and how is actual cost settled? |
Inbound: an external agent calls your paid MCP tool
A managed delegated request already projects the approving user's economics identity while preserving the external client actor. The app can therefore put paid work under the same guard used by a UI, REST operation, background job, or chat turn:
external client tools/call
|
v
managed MCP authorization
delegate actor + grantor/economics projection
|
v
EconomicsGuard
verify plan/quota/funding -> reserve estimate
|
v
tracked paid service
emit provider/model/units/cost in this scope
|
v
settle actual cost or release reservation
That is inbound payment control: the app knows the accountable operation, the projected subject, and the tracked cost, so it can admit and settle the request. It is not "MCP billing." The paid service must still report usage and a price.
Outbound: your agent calls somebody else's MCP server
The outgoing call runs inside the current KDCube request context, so paid KDCube
services invoked inside that context keep their accounting lineage. But KDCube
cannot infer what an arbitrary remote MCP vendor charged. If that remote call
must affect a KDCube budget, wrap it in an instrumented adapter that reports
supported usage or actual cost_usd, then place the call under economics
enforcement.
Transport choice is never a price declaration:
same MCP call
could be free local lookup
could invoke an accounted KDCube embedding
could call a metered third-party API
The implementation, not the tools/call envelope, determines accounting.
14 A practical decision table
| Need | Use |
|---|---|
| Connect one remote MCP server to one KDCube agent | register under as_consumer.mcp.services; attach one kind: mcp allow-list to that agent |
| Give several agents different views of one server | one registry entry; separate per-agent allowed lists |
| Bind MCP tools into LangGraph/CrewAI/custom agent | adapt registry + resolved agent policy into that framework; do not duplicate config authority |
| Expose product-specific tools from an app | KDCubeMCPServer + one @mcp entrypoint method |
| Make an endpoint intentionally public | omit managed/app-owned auth and keep work strictly bounded |
| Use an app-specific API key or header | auth.mode: bundle; app owns verification and policy |
| Let external clients connect by user consent | auth.mode: managed + Connection Hub resource/tool/grant catalog |
| Share one object grammar across apps/agents/UI | optional named-service provider + generic named-services MCP bridge |
| Charge for work behind an MCP tool | tracked service + pricing + EconomicsGuard under projected identity |
| Deliver large files | product ref or short-lived URL; local ReAct file declaration only when file exists in that workspace |
15 Regression checklist
Test the boundaries independently. A green MCP tool call alone is not enough.
Consumer
- One registered server plus one agent
kind: mcpentry produces only the expectedmcp.<alias>.<tool>ids. - Removing a tool from
allowedremoves it from that agent's catalog. - A second agent can use a narrower list without duplicating endpoint/auth.
- A missing
server_id, unreachable container URL, or interactive auth profile fails closed without leaking credentials. - In isolated execution, the executor has no MCP token and the trusted supervisor performs the call.
Provider
server/discover,tools/list, and a bounded read-onlytools/callwork through the MCP 2026-07-28 path; legacyinitializereaches the same tools.- Repeat both modes across a worker restart; no protocol session is required.
- Managed endpoint without a bearer returns the protected-resource challenge.
- Valid consent permits a selected tool; an unselected tool fails before its body runs.
- The same token fails against a different concrete MCP resource.
- Revocation stops the next call.
- App data reads use projected platform/grantor identity, not caller-supplied user ids.
- Several eligible provider accounts return
account_requiredand labeled candidates; a retry carries oneaccount_idthrough preflight and execution. - An explicitly selected but unbound account returns
agent_account_binding_requiredwith the exact caller-card recovery path; the denied operation is not replayed automatically.
Economics and data
- A paid inbound call records delegate provenance and charges the projected economics subject.
- Denied economics leaves no paid service call and no abandoned reservation.
- Remote provider cost is absent unless an adapter explicitly reports it.
- Large binaries are refs/URLs, not base64 in model context.
- Retried write calls do not duplicate side effects.
16 The builder path, in order
For consumption:
1. register server once
2. attach explicit tools to one agent
3. verify model-facing catalog
4. add credential reference
5. test isolation and result size
6. instrument cost only if the operation is paid
For exposure:
1. wrap existing async domain methods as KDCubeMCPServer tools
2. expose one @mcp alias; keep the stateless default explicit in tests
3. choose public, app-owned, or managed auth
4. for managed auth, publish Connection Hub resource/tool/grant policy
5. use projected identity in product queries
6. add accounting/economics for paid work
7. add named-service semantics only if a reusable object realm is useful
MCP should make the integration easy. The surrounding runtime should make the easy integration safe to operate.