See an app serve your data
[⏱️ ~5 min] Scaffold a Worker and an app with the CLI, run the stack, and watch the app answer an HTTP request with live fleet data
If Kernel, Gateway, Worker, or app are unfamiliar, read terminology first.
Overview
The Gateway serves no data routes of its own. It routes HTTP requests to apps, so every endpoint your application reads comes
from an app your stack declares. This tutorial scaffolds one with the mdk CLI and runs it so you can watch that happen once
before writing your own.
What you'll have at the end:
- A mock miner Worker with one seed device, scaffolded by
mdk create worker - An app with one read-only route, scaffolded by
mdk create appand running in its own process - A Gateway on
:3847forwardingGET /apps/fleet/api/fleet/summaryto that app - The JSON body of that request printed to your terminal, counting the Workers and devices the Kernel knows about
Prerequisites
- Node.js >=24 (LTS)
- npm 11 (< 12)
- The
mdkCLI installed
Create a project
mkdir fleet-demo && cd fleet-demoWrite a minimal mdk.yaml:
apiVersion: mdk/v1
kind: Stack
metadata:
name: fleet-demo
spec:
workers: []
gateway:
port: 3847
apps: []mdk onboard writes the same file interactively; this tutorial keeps it to the fields it uses.
Scaffold a Worker and an app
mdk create worker demo-miner
mdk create app fleetEach command copies a template, links the package into the project's package.json, runs npm install, and adds an entry to
mdk.yaml: the Worker under spec.workers with a mock: true seed device, the app under spec.apps.
Run the stack
mdk runmdk run boots the Kernel, the Worker and its mock device, one runtime process for the app, then the Gateway. Leave it running
and call the route from a second terminal:
curl http://localhost:3847/apps/fleet/api/fleet/summaryExpected output (ids vary):
{"ok":true,"kernelConnected":true,"workerCount":1,"deviceCount":1,"workers":[{"workerId":"...","devices":1}]}A workerCount of 0 means the Worker has not registered yet; call again after a few seconds. That JSON came from a route the
Gateway did not have until the stack declared an app that serves it.
Read the files that produced it
The app is a directory with a manifest, a route table and a controller:
apps/fleet/
├── mdk-app.yaml # id, version, displayName, permissions, config defaults
└── plugin/
├── mdk-plugin.json # the route table the Gateway reads
├── lib/client.js # the app's MDK client
└── controllers/summary.js # the handlerplugin/mdk-plugin.json declares the route: an id, the handler path, an http block giving the method and path, and
"safety": "read-only", which is what lets mdk run mcp offer the route to an agent as a tool.
plugin/controllers/summary.js is the handler. Every controller is async function (req) and returns a plain object, which
is sent back as JSON. It never touches a response object:
const mdkClient = require('../lib/client')
module.exports = async function summary (req) {
const resp = await mdkClient.listWorkers()
const workers = (resp && resp.workers) || []
// count devices across every Worker
}mdkClient, built once for the app in plugin/lib/client.js from require('@tetherto/mdk-gateway/plugin'), is the same
protocol client used everywhere else in MDK. Aggregation lives here, in the app. Workers are structurally single-device,
so a Worker can only ever answer for one device; combining them into a fleet view is the app's job.
The route you just called accepted the request without a token, and every route without "auth": true behaves the same
way. Protecting a route is covered in auth and permissions.
What just happened
- Scaffolding copied a Worker template (a firmware-v3 miner simulator) and an app template into
workers/demo-minerandapps/fleet, and registered both inmdk.yaml. - Kernel started fail-closed. Each caller, the app runtime included, published its key to
.mdk/trusted-keys/, which is how the Kernel admits it. - Worker started with its mock device and published its key to
.mdk/keys/, where the Kernel discovered it. - App runtime started
apps/fleetin its own process, loaded its route table and controller, and accepted calls only from the Gateway's key. - Gateway read the app's
mdk-plugin.json, mounted its route under/apps/fleet, and forwarded your request to the app runtime over HRPC. - The controller called
listWorkers()on the Kernel and returned the counts as JSON.
Cleanup
Stop mdk run with Ctrl+C, then remove the project:
cd .. && rm -rf fleet-demoNext steps
- Write your own routes by building an app: the manifests, controllers, reading hardware, and sending commands
- Put a page in front of your route by building a minimal single-page dashboard: the same shape with a React front end
- Integrate your own hardware by building a third-party Worker
- See the whole stack instead of one route by running a mining site end to end