MCP in KDCube: Connect Any Agent, Expose Any App, Govern Every Call
A technical map for consuming and exposing MCP in KDCube: per-agent tool inventories, FastMCP app surfaces, managed delegated credentials, named services, files, accounting, and economics.
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-native FastMCP surface; 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. A client 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 FastMCP 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
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.
02 Direction 1: connect an MCP server to an 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.
03 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 with
langchain-mcp-adapters: load MCP tools asynchronously, bind them alongside
plain LangChain tools, and degrade cleanly when the optional adapter is absent.
The canonical production configuration remains the two-part registry +
per-agent policy above.
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.
04 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.
05 Direction 2: expose MCP from any KDCube app
The provider path is equally small. Build an ordinary FastMCP app and expose it
through one @mcp method.
from mcp.server.fastmcp import FastMCP
from kdcube_ai_app.infra.plugin.bundle_loader import mcp
def build_reports_mcp(service):
server = FastMCP("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 today. stateless_http=True 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. A FastMCP session object in one worker is not a
durable boundary.
App tools remain ordinary async product code. FastMCP supplies protocol discovery and input schemas; KDCube supplies loading, routing, request context, descriptor policy, secrets, observability, and optional managed authorization and economics.
06 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.
07 What happens before a managed tool runs
The proc MCP bridge resolves the app and its effective descriptor before it
calls the FastMCP 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 -> FastMCP 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.
08 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.
09 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.
10 Named services are optional, not a toll booth
MCP and named services solve different problems.
Use app-native FastMCP 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.
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.
11 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.
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.
12 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.
13 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 | FastMCP + 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 |
14 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
initialize,tools/list, and a bounded read-onlytools/callsurvive a worker restart because the FastMCP app is stateless.- 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.
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.
15 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 FastMCP tools
2. expose one @mcp alias with stateless_http=True
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.