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 is running
- A model is served on this machine, or reachable on another, because the app dials the provider rather than starting one
- An MCP tool server is reachable, so the agent has fleet tools to call
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:
1.1 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: /.
Until @tetherto/mdk-app-agent is on npm, install it from its repository with
mdk add app github:tetherto/mdk-app-agent, which writes the same spec.apps entry, mount: / included.
1.1 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
rbac:
enforcement: deny
roles:
- name: agent-operator
rules:
- resources: [agent/*]
verbs: [get, list, create, update, delete]
bindings:
- role: agent-operator
subjects: [{ kind: ServiceAccount, name: operator-agent }]spec.rbac is what lets a caller reach the app's routes, and a stack without one denies every route. A
route is governed as <app>/<path>, so agent/* covers everything this app serves whatever its paths are. To name
routes individually instead, take them from the app's own route table and write them as
resources and verbs inside a role and its bindings. mdk rbac apply hands an
edited policy to a Kernel that is already running, and a binding's subject is
a user or a service account you create.
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:
mdk sa add operator-agent # the credential the caller below signs in with; the Kernel registers it at the next start
mdk rbac init # binds the CLI's own account and grants the app the Kernel access its manifest asks for
mdk runmdk run refuses to boot a Kernel that nobody can administer, so without mdk rbac init it exits with
spec.rbac binds nobody. mdk run then 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.
Every agent route needs a bearer token, local caller or not, and mdk run refuses a gateway.config.authVerifier. A person signs
in at /auth/login with a user name and password, which mdk user add creates; the non-interactive caller below signs
in as the operator-agent service account the policy binds
(Gateway security model).
A missing config.agent block answers 503 ERR_AGENT_UNAVAILABLE instead of
failing to load.
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.
TOKEN=$(mdk sa token operator-agent)
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 itsurl) is missing from the config — how to fix it is in the app's troubleshooting entry - The agent routes answer
404. The app entry has nomount: /, so its routes are served under/apps/<name>. Addmount: /to itsspec.appsentry - Every other failure (
503,404,409,400) maps to a specific cause and fix in the app's error reference npm cifails to resolve a stack that mounts only this app.@tetherto/mdk-app-agentdeclares@tetherto/mdk-agentas a required dependency, not an optional peer, so the agent package must be installed alongside it fornpm cito resolve.
Next steps
- Read the agent app's route reference: session, message, and approval routes, plus the manifest's
setupfields - Understand the underlying agent: the model and its fleet tools
- Submit and approve write actions from a React app, for the UI-driven shape of this same approval gate