MDK Logo

Deploy the agent behind the Gateway

Mount the conversational operator agent behind the Gateway as a chat API, and drive a session through an approval-gated write

Overview

@tetherto/mdk-app-agent mounts @tetherto/mdk-agent behind the Gateway as a chat API.

Enable the app for session, message, and approval routes: every write the agent proposes pauses for an operator's decision. The agent itself still reaches fleet data the way any AI agent does, over an MCP server reachable at agent.mcp.url — the standalone @tetherto/mdk-mcp process; this app gives a human operator a chat surface to talk to it through.

This is one of two ways to run the agent. If you've already run it standalone, it's the same agent with HTTP on it, not a second product: the same provider, the same MCP client, the same approval gate, just reached over sessions instead of a REPL.

Prerequisites

The Gateway never starts a model. config.agent.provider is a URL it dials, so a first session against a baseURL with nothing behind it fails with ERR_AGENT_UNAVAILABLE. Serving a model locally with QVAC covers the install, the first-run download, and the flags a lazily loaded model needs.

Mount the app

Either:

A Select it during onboarding

Run mdk onboard and select mdk-app-agent from its app catalog. mdk onboard's wizard supports creating a compliant mdk.yaml from scratch, interactively. It installs @tetherto/mdk-app-agent by package name and writes its spec.apps entry with mount: /.

The catalog entry installs the package by name from npm, where it is not published, so that install fails. Install it from its public repository instead, with mdk add app github:tetherto/mdk-app-agent, which writes the same spec.apps entry, mount: / included. That entry carries serviceAccount: app:agent and a permissionsDigest, and the install also writes a credential under .mdk/credentials/apps/. The dependency it records in package.json is the github: specifier, carrying no version or ref, so the commit is pinned by your lockfile rather than by the manifest. Commit the lockfile: without one, a later install resolves whatever that repository's default branch points at.

B Add it to an existing mdk.yaml

Already have one from a previous onboarding run? Add the app under spec.apps by hand, with mount: / and the model provider, the MCP url, and the approval timeout under agent. The app resolves from node_modules/@tetherto/mdk-app-agent. For example, a standalone gateway carrying just this app, reaching across to the MVP site example's MCP tool server:

apiVersion: mdk/v1
kind: Stack
metadata:
  name: agent-gateway
spec:
  workers: []
  gateway:
    port: 3847
  apps:
    - name: agent
      package: "@tetherto/mdk-app-agent"
      mount: /
      config:
        agent:
          provider: { kind: qvac, model: qwen3-4b, baseURL: http://127.0.0.1:11500/v1 }
          mcp: { url: http://127.0.0.1:3101/mcp }
          approvalTimeoutMs: 120000

Keep mount: /. The agent UI calls /agent/sessions… at the Gateway root; without it the routes live under /apps/agent/agent/sessions… and the UI gets 404.

1.2 Run it

Once the mdk.yaml names the app, either way, write the access policy before the first run:

mdk rbac init

mdk run refuses a stack whose spec.rbac binds nobody, because the Kernel would boot with nobody able to administer it. mdk rbac init writes the starter policy: an admin role, the CLI bound to it, and the Gateway and MCP server bound to read policy. Step 4 of the security blueprint covers what it writes and how to change it.

Then run mdk run. It starts the app in its own runtime process and the Gateway forwards the agent routes to it.

A stack built for just this app declares no Workers, so mdk run boots a Kernel it never actually uses (it only proxies chat to the MVP site's separate MCP server) alongside the app and the Gateway.

The agent routes are auth: true, so each one is checked against spec.rbac and needs an access token. A call without one answers 401 ERR_AUTH_UNAUTHORIZED. Create the first operator with mdk user add <name> --admin while the stack is running, then sign in at POST /auth/login and send the accessToken it returns as Authorization: Bearer <token> (Gateway security model). A missing config.agent block answers 503 ERR_AGENT_UNAVAILABLE instead of failing to load.

This app requests no Kernel access of its own, so mdk run warns that app agent has no binding in spec.rbac on every start, even once mdk rbac init has run and reports the grant already in place. There is nothing to bind for an app with no permissions, and the warning does not stop the app serving. Running mdk rbac init again changes nothing.

Create a session and send a message

Use the port your mdk.yaml gave the gateway: 3847 in the example above. Note that the MVP site's own gateway (3000) never carries the agent app; this is a separate gateway, reaching across to the MVP site's MCP tool server on 3101.

Sign in first. The access token comes back in the response body; every call below carries it.

TOKEN=$(curl -s -X POST http://localhost:<port>/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"<name>","password":"<password>"}' | jq -r .accessToken)

curl -X POST http://localhost:<port>/agent/sessions \
  -H "Authorization: Bearer $TOKEN"
# {"sessionId":"..."}

curl -N -X POST http://localhost:<port>/agent/sessions/<id>/messages \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"text":"how many miners are on the site?"}'

The response streams as text/event-stream. A read-only question ends in tool_call, tool_result, token, and done events, each stamped with the turn's turnId and a monotonic seq.

Approve a write

A write action pauses the turn instead of running it:

event: pending_approval
data: {"type":"pending_approval","name":"act_device","args":{"ref":"whatsminer-0","action":"reboot"},"approvalId":"..."}

Decide it from the paused stream's approvalId:

curl -X POST http://localhost:<port>/agent/sessions/<id>/approvals/<approvalId> \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"approved":true}'

Approving resumes the same stream: the tool runs for real, and the turn continues to its token and done events. Rejecting, or letting the approval window expire, resolves to false, and the write never runs.

Troubleshooting

  • Sessions and messages work, but the agent never calls a tool. agent.mcp (or its url) is missing from the config — how to fix it is in the app's troubleshooting entry
  • Every agent route answers 401. No access token on the call, or an expired one. Sign in again at POST /auth/login and resend with Authorization: Bearer <token>
  • The agent routes answer 404. The app entry has no mount: /, so its routes are served under /apps/<name>. Add mount: / to its spec.apps entry
  • Every other failure (503, 404, 409, 400) maps to a specific cause and fix in the app's error reference
  • npm ci fails to resolve a stack that mounts only this app. @tetherto/mdk-app-agent declares @tetherto/mdk-agent as a required dependency, not an optional peer, so the agent package must be installed alongside it for npm ci to resolve.

Next steps

On this page