MDK Logo

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 app and running in its own process
  • A Gateway on :3847 forwarding GET /apps/fleet/api/fleet/summary to that app
  • The JSON body of that request printed to your terminal, counting the Workers and devices the Kernel knows about

Prerequisites

Create a project

mkdir fleet-demo && cd fleet-demo

Write 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 fleet

Each 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 rbac init
mdk run

mdk rbac init writes the roles and bindings spec.rbac needs, binding the CLI's own account to an admin role and granting the app the Kernel access its manifest requests. Without them mdk run refuses to boot a Kernel nobody could administer.

mdk run boots the Kernel, the Worker and its mock device, one runtime process for the app, then the Gateway. Leave it running. No app route is open, so create a user from a second terminal, sign in, and call the route with the access token:

mdk user add admin --admin   # prompts for a password; the Kernel must still be running

TOKEN=$(curl -s -X POST http://localhost:3847/auth/login \
  -H 'content-type: application/json' \
  -d '{"username":"admin","password":"<the password you set>"}' | jq -er .accessToken)

curl -s -H "Authorization: Bearer $TOKEN" http://localhost:3847/apps/fleet/api/fleet/summary

Expected 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 handler

plugin/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 answered because --admin bound admin to a role holding wildcards. Every app route is checked against spec.rbac: a request with no token answers 401, and a signed-in caller with no rule over fleet/api/fleet/summary answers 403. Narrowing a user down to the resources they should reach is covered in auth and permissions.

What just happened

  1. Scaffolding copied a Worker template (a firmware-v3 miner simulator) and an app template into workers/demo-miner and apps/fleet, and registered both in mdk.yaml.
  2. Kernel started fail-closed. Each caller, the app runtime included, published its key to .mdk/trusted-keys/, which is how the Kernel admits it. Admission only lets a caller connect: every request past sign-in carries an access token the Kernel checks against spec.rbac.
  3. Worker started with its mock device and published its key to .mdk/keys/, where the Kernel discovered it.
  4. App runtime started apps/fleet in its own process, loaded its route table and controller, and accepted calls only from the Gateway's key.
  5. Gateway signed admin in at /auth/login, checked the access token and the rule over fleet/api/fleet/summary, then read the app's mdk-plugin.json, mounted its route under /apps/fleet, and forwarded your request to the app runtime over HRPC.
  6. 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-demo

Next steps

On this page