MDK Logo

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 use npx mdk, which runs the CLI the project's package.json depends on.
  • The stack running: npm run dev, the same as npx mdk run

Create the agent's service account

npx mdk sa add operator-agent

This 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-agent

An 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 apply

The 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/mcp

It 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

SymptomCauseFix
401 from /mcpNo token, or an expired oneSend authorization: Bearer <token> with a fresh token
tools/list returns no tools, and a call answers Tool ops_summary not foundNo rule grants the agent any routeBind it in spec.rbac, then run npx mdk rbac apply
ERR_AUTH_FORBIDDEN: operator-agent may not get ops/api/ops/summary from the GatewayThe agent's rules don't cover that resource and verbWiden the rule's resources or verbs
ERR_APP_RUNTIME_UNAVAILABLEThe app isn't runningStart the stack with npm run dev

Next steps

On this page