Connect an AI agent over MCP
Give an AI agent its own service account, grant it app routes in spec.rbac, and call those routes as MCP tools
Overview
Any MCP client, such as a coding agent, an agent framework or a script, can call an MDK stack's app routes as tools.
The agent signs in as its own service account, spec.rbac decides which routes it may call, and mdk run mcp serves
those routes as MCP tools on /mcp. Every request carries the agent's bearer token, so each agent sees
only the tools its role allows. This page connects one agent to the ops app that mdk create app ops scaffolds.
Prerequisites
- An MDK project with an app, for example from
npx mdk create app ops. Commands on this page usenpx mdk, which runs the CLI the project'spackage.jsondepends on. - The stack running:
npm run dev, the same asnpx mdk run
Create the agent's service account
npx mdk sa add operator-agentThis writes the agent's Ed25519 credential to .mdk/credentials/agents/operator-agent.json, readable by you only, and
registers its public key with the running Kernel. The account can't call anything yet.
Grant it routes in spec.rbac
Add a role to mdk.yaml and bind the agent to it, beside the roles and bindings mdk onboard or mdk rbac init wrote:
spec:
rbac:
roles:
- name: ops-viewer
rules:
- resources: [ops/*]
verbs: [get, list]
bindings:
- role: ops-viewer
subjects:
- kind: ServiceAccount
name: operator-agentAn app route's resource is <app>/<path>: the app's name in spec.apps, then the route's path. GET /api/ops/summary
in the ops app is ops/api/ops/summary, so ops/* covers every route of the app. The verb is the route's verb, or
follows its method: GET is get, POST is create, PUT and PATCH are update, DELETE is delete.
Apply the policy:
npx mdk rbac applyThe running Kernel switches to the new policy. Nothing restarts, the MCP server included.
Start the MCP server
In a second terminal:
npx mdk run mcp # http://localhost:3947/mcpIt listens on spec.mcp.port, by default the Gateway's port plus 100. Only routes marked "safety": "read-only"
become tools unless it starts with --allow-write.
Get the agent a token
For a quick test, print one. It lasts 10 minutes:
TOKEN=$(npx mdk sa token operator-agent)An agent that runs longer signs itself in with its credential and renews the token before it expires.
@tetherto/mdk-identity does both, through the Gateway's POST /auth/token. Ask the token source for headers on every
POST /mcp:
const fs = require('node:fs')
const { ServiceAccountTokenSource, gatewayTokenRequest } = require('@tetherto/mdk-identity')
const { name, privateKeyPem } = JSON.parse(fs.readFileSync('.mdk/credentials/agents/operator-agent.json', 'utf8'))
const source = new ServiceAccountTokenSource({
name,
privateKey: privateKeyPem,
request: gatewayTokenRequest('http://localhost:3847')
})
async function mcpHeaders () {
return { authorization: `Bearer ${await source.getToken()}` }
}Call the tools
List them:
curl -s -X POST localhost:3947/mcp \
-H 'content-type: application/json' -H 'accept: application/json, text/event-stream' \
-H "authorization: Bearer $TOKEN" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'The result lists ops_summary, named after the route's id, with the route's description as its own. Call it:
curl -s -X POST localhost:3947/mcp \
-H 'content-type: application/json' -H 'accept: application/json, text/event-stream' \
-H "authorization: Bearer $TOKEN" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"ops_summary","arguments":{}}}'The tool returns the controller's JSON, {"ok":true,"kernelConnected":true,"workerCount":1,"deviceCount":1,…}: the same
body the Gateway returns for GET /apps/ops/api/ops/summary with the same token.
To connect an MCP client, point it at http://localhost:3947/mcp with the authorization header. A client with a fixed
header needs a fresh token every 10 minutes; one that runs your code can use the token source above.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
401 from /mcp | No token, or an expired one | Send authorization: Bearer <token> with a fresh token |
tools/list returns no tools, and a call answers Tool ops_summary not found | No rule grants the agent any route | Bind it in spec.rbac, then run npx mdk rbac apply |
ERR_AUTH_FORBIDDEN: operator-agent may not get ops/api/ops/summary from the Gateway | The agent's rules don't cover that resource and verb | Widen the rule's resources or verbs |
ERR_APP_RUNTIME_UNAVAILABLE | The app isn't running | Start the stack with npm run dev |
Next steps
- Choose which routes become tools: read-only routes by default, and tools written by hand
- Secure MCP for agents beyond localhost: TLS,
allowedHostsand one service account per agent - Deploy the operator agent behind the Gateway: MDK's own chat agent, for people who signed in