@tetherto/mdk-skill 0.0.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +133 -0
  3. package/dist/skills/mdk/SKILL.md +114 -0
  4. package/dist/skills/mdk/references/architecture.md +89 -0
  5. package/dist/skills/mdk/references/glossary.md +27 -0
  6. package/dist/skills/mdk/references/mdk-contract.schema.json +138 -0
  7. package/dist/skills/mdk/references/package-index.md +53 -0
  8. package/dist/skills/mdk/references/protocol.md +95 -0
  9. package/dist/skills/mdk-create-app/SKILL.md +231 -0
  10. package/dist/skills/mdk-deployment/SKILL.md +161 -0
  11. package/dist/skills/mdk-gateway-plugin/SKILL.md +192 -0
  12. package/dist/skills/mdk-gateway-plugin/references/controller-patterns.md +99 -0
  13. package/dist/skills/mdk-gateway-plugin/references/plugin-authoring.md +121 -0
  14. package/dist/skills/mdk-publish-security/SKILL.md +177 -0
  15. package/dist/skills/mdk-publish-security/references/vuln-classes.md +151 -0
  16. package/dist/skills/mdk-publish-security/scripts/scan-packages.mjs +544 -0
  17. package/dist/skills/mdk-site-sizing/SKILL.md +160 -0
  18. package/dist/skills/mdk-site-sizing/eval/held-out.json +104 -0
  19. package/dist/skills/mdk-site-sizing/eval/profiles/cap-100devices-2workers-mixed.json +24 -0
  20. package/dist/skills/mdk-site-sizing/eval/profiles/cap-100devices-2workers.json +23 -0
  21. package/dist/skills/mdk-site-sizing/eval/profiles/cap-10devices-1workers.json +23 -0
  22. package/dist/skills/mdk-site-sizing/eval/profiles/cap-15devices-2workers.json +24 -0
  23. package/dist/skills/mdk-site-sizing/eval/profiles/cap-200devices-4workers.json +23 -0
  24. package/dist/skills/mdk-site-sizing/eval/profiles/cap-500devices-4workers.json +23 -0
  25. package/dist/skills/mdk-site-sizing/eval/profiles/cap-50devices-2workers.json +23 -0
  26. package/dist/skills/mdk-site-sizing/references/envelope.json +284 -0
  27. package/dist/skills/mdk-site-sizing/references/envelope.md +42 -0
  28. package/dist/skills/mdk-site-sizing/scripts/compile-envelope.mjs +45 -0
  29. package/dist/skills/mdk-site-sizing/scripts/eval-held-out.mjs +50 -0
  30. package/dist/skills/mdk-site-sizing/scripts/lib.mjs +591 -0
  31. package/dist/skills/mdk-site-sizing/scripts/parse-site.mjs +25 -0
  32. package/dist/skills/mdk-site-sizing/scripts/size-site.mjs +49 -0
  33. package/dist/skills/mdk-ui-component/SKILL.md +207 -0
  34. package/dist/skills/mdk-ui-component/references/page-recipe.md +78 -0
  35. package/dist/skills/mdk-ui-component/references/ui-registry.json +1 -0
  36. package/dist/skills/mdk-worker-plugin/SKILL.md +198 -0
  37. package/dist/skills/mdk-worker-plugin/assets/mdk-contract.template.json +45 -0
  38. package/dist/skills/mdk-worker-plugin/assets/worker-template/README.md +93 -0
  39. package/dist/skills/mdk-worker-plugin/assets/worker-template/mdk-contract.json +26 -0
  40. package/dist/skills/mdk-worker-plugin/assets/worker-template/mock/server.js +93 -0
  41. package/dist/skills/mdk-worker-plugin/assets/worker-template/package.json +23 -0
  42. package/dist/skills/mdk-worker-plugin/assets/worker-template/smoke.config.js +41 -0
  43. package/dist/skills/mdk-worker-plugin/assets/worker-template/src/client.js +47 -0
  44. package/dist/skills/mdk-worker-plugin/assets/worker-template/src/commands/reboot.js +5 -0
  45. package/dist/skills/mdk-worker-plugin/assets/worker-template/src/commands/set-power-mode.js +10 -0
  46. package/dist/skills/mdk-worker-plugin/assets/worker-template/src/telemetry/hashrate-rt.js +5 -0
  47. package/dist/skills/mdk-worker-plugin/assets/worker-template/src/telemetry/power-mode.js +5 -0
  48. package/dist/skills/mdk-worker-plugin/assets/worker-template/src/telemetry/power.js +5 -0
  49. package/dist/skills/mdk-worker-plugin/assets/worker-template/src/telemetry/temperature.js +5 -0
  50. package/dist/skills/mdk-worker-plugin/references/contract-authoring.md +135 -0
  51. package/dist/skills/mdk-worker-plugin/references/device-families.md +96 -0
  52. package/dist/skills/mdk-worker-plugin/references/local-testing.md +134 -0
  53. package/dist/skills/mdk-worker-plugin/references/mdk-contract.schema.json +138 -0
  54. package/dist/skills/mdk-worker-plugin/references/worker-base-api.md +152 -0
  55. package/dist/skills/mdk-worker-plugin/scripts/validate-contract.mjs +129 -0
  56. package/dist/skills/mdk-worker-plugin/scripts/worker-smoke.mjs +256 -0
  57. package/package.json +47 -1
  58. package/src/assemble.mjs +11 -0
  59. package/src/index.d.ts +57 -0
  60. package/src/index.mjs +254 -0
  61. package/src/install.mjs +57 -0
  62. package/src/sources.map.json +34 -0
@@ -0,0 +1,161 @@
1
+ ---
2
+ name: mdk-deployment
3
+ description: >
4
+ Deploy and run a working MDK stack from mdk.yaml. Use when the task mentions
5
+ "deploy", "run the stack", "start the Kernel + Workers + Gateway",
6
+ "register a plugin", "register an app", "mdk.yaml", or MDK environment configuration.
7
+ metadata:
8
+ suite: mdk-developer-skill
9
+ mdk_version: "0.5.0"
10
+ license: Apache-2.0
11
+ ---
12
+
13
+ # Deploy and run an MDK stack
14
+
15
+ In CLI-managed projects (this folder), the stack is declared in `mdk.yaml` and
16
+ started with `mdk run`. Boot order matters: **Kernel → Workers → apps → Gateway/UI**.
17
+
18
+ ## Project shape
19
+
20
+ ```
21
+ <project>/
22
+ ├── mdk.yaml # Stack spec (source of truth)
23
+ ├── package.json # private root manifest; links workers/<name> and apps/<name> by file: deps
24
+ ├── workers/<name>/ # Worker Plugin packages (mdk create worker)
25
+ ├── apps/<name>/ # apps: mdk-app.yaml + plugin/ route table (mdk create app)
26
+ ├── apps/dashboard/ # UI shell (mdk create dashboard)
27
+ └── .mdk/ # runtime state written by `mdk run` — gitignored, disposable
28
+ ```
29
+
30
+ Workers and apps are linked from the root manifest by relative `file:`
31
+ dependencies: one root `npm install` links every component into the project's
32
+ `node_modules`, which is how `mdk run` resolves them. The dashboard is
33
+ deliberately **not** linked — it links the MDK UI packages by `file:` path and
34
+ installs on its own, so its React is never hoisted alongside those links.
35
+
36
+ ## `mdk.yaml` essentials
37
+
38
+ ```yaml
39
+ apiVersion: mdk/v1
40
+ kind: Stack
41
+ metadata:
42
+ name: <stack-name>
43
+ spec:
44
+ kernel:
45
+ port: 3848
46
+ gateway:
47
+ port: 3847
48
+ apps:
49
+ - name: <app-name> # ^[a-z][a-z0-9-]*$; routes served under /apps/<app-name>
50
+ package: <app-package-name> # e.g. fleet-summary, or @org/name
51
+ config: {} # overrides the mdk-app.yaml config defaults
52
+ # mount: / # serve at the Gateway root (required for @tetherto/mdk-app-agent)
53
+ mcp:
54
+ port: 3947 # optional — defaults to gateway.port + 100
55
+ workers:
56
+ - name: <worker-name>
57
+ package: ./workers/<worker-name> # local path, or an installed npm name
58
+ config:
59
+ mock: true
60
+ devices:
61
+ - id: dev-0
62
+ opts: { host: 127.0.0.1, port: 18080, serial: DEV-0 }
63
+ ```
64
+
65
+ Both `mdk create worker <name>` and `mdk create app <name>` scaffold the
66
+ package **and** register it here automatically (Workers under
67
+ `spec.workers`, apps under `spec.apps`) — no manual
68
+ `package.json` linking step. `create worker` also seeds a stack-unique mock port/device id
69
+ per Worker, so scaffolding several in one project never collides. A published
70
+ package goes through `mdk add app <npm-or-github-source>` / `mdk add worker <source>`,
71
+ which installs it, validates it, and only then adds the entry. If you add a
72
+ package by hand instead, mirror that: `npm install` at the project root, append the
73
+ entry yourself, then check it with `mdk validate app <name>` / `mdk validate worker <name>`.
74
+
75
+ `spec.gateway.plugins` is rejected by the CLI; move each entry to `spec.apps` with a `name`.
76
+
77
+ ## Run commands
78
+
79
+ ```bash
80
+ mdk run # whole stack from mdk.yaml: kernel + workers + one process per app + gateway
81
+ mdk run kernel
82
+ mdk run gateway
83
+ mdk run worker <name> # e.g. mdk run worker demo-miner
84
+ mdk run app <name> # one app runtime, in the foreground
85
+ mdk run dashboard # the scaffolded UI shell's dev server
86
+ mdk run mcp # standalone MCP server for AI agents (needs Kernel running first)
87
+ ```
88
+
89
+ `mdk run` with no target boots everything together; that's not a spec setting,
90
+ just which command you type. Use the per-component targets and separate
91
+ terminals when debugging one tier. Gateway port is `spec.gateway.port`
92
+ (example: `3847`); curl an app at `http://127.0.0.1:<gateway-port>/apps/<app-name>/api/...`
93
+ (or at `/api/...` when its entry sets `mount: /`). Each app runs in its own runtime process
94
+ and accepts calls only from the Gateway's key; the Gateway forwards each request to it.
95
+
96
+ Like the dashboard, `mcp` is never part of the default `all` target — always
97
+ start it explicitly, in its own terminal, after `mdk run kernel` (a standalone
98
+ `run mcp` has no in-process Kernel handle to reuse). It reads `spec.apps` and exposes each
99
+ declared app's routes as agent-callable tools, over `spec.mcp.port` if set,
100
+ else `spec.gateway.port + 100`. Routes not marked `"safety": "read-only"` are withheld
101
+ unless it is started with `--allow-write`.
102
+
103
+ Every caller publishes its Kernel key to `.mdk/trusted-keys/` (`gateway.pub`, `mcp.pub`,
104
+ `cli.pub`, `app-<name>.pub`, …) and the fail-closed Kernel admits it with full access: an app's
105
+ `permissions` in `mdk-app.yaml` are not enforced yet.
106
+
107
+ Ctrl+C / SIGTERM stops components in reverse boot order (Gateway → apps → Workers →
108
+ Kernel) and always exits, even if a component's own shutdown hangs.
109
+
110
+ ### Verify
111
+
112
+ ```bash
113
+ mdk status # environment + Kernel/Gateway/worker health, exit code encodes it
114
+ mdk status -o json | jq .health
115
+ curl -s http://127.0.0.1:3847/apps/<app-name>/api/<route> | jq .
116
+ ```
117
+
118
+ `mdk status` never starts, stops or repairs anything — it probes the Kernel
119
+ over HRPC and the Gateway over HTTP, reports declared-vs-registered Workers, and
120
+ counts an app as running while a live process holds its run lock.
121
+ Exit codes: `0` healthy, `4` precondition not met (bad Node, invalid
122
+ `mdk.yaml`, packages not installed), `5` stack not fully up.
123
+
124
+ UI: `mdk run dashboard` (or `cd apps/dashboard && npm run dev` directly) and
125
+ open the page route added by `mdk-ui-component`.
126
+
127
+ ## Scaffolding helpers
128
+
129
+ | Need | Command |
130
+ | ------------------ | ------------------------------------------------------ |
131
+ | Guided `mdk.yaml` | `mdk onboard` |
132
+ | New Worker package | `mdk create worker <name>` → then `mdk-worker-plugin` |
133
+ | New app | `mdk create app <name>` (`mdk-create-app`) → then `mdk-gateway-plugin` for its routes |
134
+ | Published app | `mdk add app <npm-or-github-source>` |
135
+ | New dashboard app | `mdk create dashboard` |
136
+
137
+ The `create` commands run `npm install` by default (`--no-install` to skip) and, for
138
+ Workers/apps, install at the project root so the new package links into the
139
+ project rather than getting its own nested `node_modules`.
140
+
141
+ ## Boot-order invariant
142
+
143
+ 1. Kernel listening.
144
+ 2. Workers announce on the DHT topic, or attach directly when run in the same process.
145
+ 3. One runtime process per app (each builds its own Kernel client).
146
+ 4. Gateway reads each app's route table and forwards requests to its runtime; an app not up yet answers `503`.
147
+ 5. UI last — it only talks to the Gateway.
148
+
149
+ Starting the UI or an app before the Worker is online yields empty arrays,
150
+ not necessarily HTTP errors — treat an empty `devices: []` in an app
151
+ response as a deployment signal, not a bug.
152
+
153
+ ## Hand-off
154
+
155
+ | Next need | Skill |
156
+ | ---------------------------------- | -------------------- |
157
+ | How many Workers / which host tier | `mdk-site-sizing` |
158
+ | New device Worker | `mdk-worker-plugin` |
159
+ | New app package | `mdk-create-app` |
160
+ | New aggregation API | `mdk-gateway-plugin` |
161
+ | New dashboard page | `mdk-ui-component` |
@@ -0,0 +1,192 @@
1
+ ---
2
+ name: mdk-gateway-plugin
3
+ description: >
4
+ Build the HTTP routes of an MDK app (route table + controllers, served
5
+ through the Gateway) over one or more MDK Workers. Use when the task
6
+ mentions "aggregate", "combine Workers",
7
+ "site summary / rollup", "plugin", "mdk-plugin.json", "Gateway endpoint",
8
+ "cross-worker endpoint", or when a UI needs live Worker data and no matching
9
+ `/api/...` route exists yet.
10
+ metadata:
11
+ suite: mdk-developer-skill
12
+ mdk_version: "0.5.0"
13
+ license: Apache-2.0
14
+ ---
15
+
16
+ # Build the routes of an MDK app
17
+
18
+ The Gateway serves HTTP routes from **apps**. An app is a small Node package with an `mdk-app.yaml` manifest
19
+ and a route table (`mdk-plugin.json`, in `plugin/` or the package root); `mdk run` runs each app in its own
20
+ process and the Gateway forwards each request to it. Each route is a controller that queries Workers through
21
+ the app's own `@tetherto/mdk-client`, built once from `require('@tetherto/mdk-gateway/plugin')`.
22
+
23
+ Apps are the **server half** of any "show me X from my devices" request.
24
+ If the UI needs data that no existing route returns, build the app's route
25
+ first, then hand off to `mdk-ui-component`. Creating the package itself (manifest,
26
+ permissions, layout) is the [`mdk-create-app`](../mdk-create-app/SKILL.md) skill; this
27
+ skill owns the route table and controllers.
28
+
29
+ ## When to use this skill
30
+
31
+ | Situation | Action |
32
+ | ----------------------------------------------------- | -------------------------- |
33
+ | UI / chart needs a metric and no Gateway route exists | Add a route here first |
34
+ | Aggregate / rollup / site summary" across devices | Add a route |
35
+ | Endpoint already exists (check `mdk.yaml` → `spec.apps`, or `mdk status`) | Skip — go straight to `mdk-ui-component` |
36
+ | Need a new device protocol | Wrong skill — use `mdk-worker-plugin` |
37
+
38
+ ## Workflow
39
+
40
+ ### 1. site capability discovery (mandatory)
41
+
42
+ Do **not** invent channel names, units, or device brands. Ground every field
43
+ in an installed Worker contract.
44
+
45
+ **Resolve each Worker's contract from `mdk.yaml` → `spec.workers[].package`:**
46
+
47
+ | `package` value | Contract path |
48
+ | --------------------------------------------- | ----------------------------------------------------------------------------------- |
49
+ | Local path (`./workers/<name>`, `../…`) | `<package>/mdk-contract.json` (or legacy `<package>/plugin/mdk-contract.json`) |
50
+ | npm name (`@tetherto/mdk-worker-avalon`, …) | `node_modules/<package>/mdk-contract.json` (or legacy `…/plugin/mdk-contract.json`) |
51
+
52
+ Also scan `workers/*/mdk-contract.json` and `workers/*/plugin/mdk-contract.json` for any local scaffolds not yet
53
+ listed in `mdk.yaml`. If an npm package is listed but missing under
54
+ `node_modules/`, run `npm install` first — do not invent the contract.
55
+
56
+ Then:
57
+
58
+ 1. Open each resolved `mdk-contract.json`.
59
+ 2. Read `capabilities.telemetry[]` / `commands[]` — note `name`, `type`, `unit`.
60
+ 3. Read `metadata.brand` / `deviceFamily` / `provider` for how to recognize
61
+ the device family at runtime.
62
+ 4. Confirm the Kernel actually has the Worker online (`mdk run worker <name>`
63
+ or `mdk status`) before assuming data flows.
64
+
65
+ `getCapabilities` returns the capability list only — not full contract
66
+ metadata (no `brand`/`provider`) — so the fingerprint you match against at
67
+ runtime is the telemetry/command **name set**, derived from the contract you
68
+ just read, not a hard-coded device list.
69
+
70
+ ### 2. Scaffold the app
71
+
72
+ ```bash
73
+ mdk create app <app-name>
74
+ ```
75
+
76
+ Scaffolds and registers in one step (manifest rules: [`mdk-create-app`](../mdk-create-app/SKILL.md)):
77
+
78
+ ```
79
+ apps/<app-name>/
80
+ ├── package.json # name: <app-name> (or @org/<app-name> with --org)
81
+ ├── mdk-app.yaml # id, version (= package.json), displayName, permissions, config defaults
82
+ └── plugin/
83
+ ├── mdk-plugin.json # routes + OpenAPI-ish response schema
84
+ ├── lib/client.js # the app's MDK client
85
+ └── controllers/
86
+ └── summary.js # async (req) => payload
87
+ ```
88
+
89
+ It also links the package into the project's root `package.json` (root `npm install`,
90
+ skip with `--no-install`) and appends it under `mdk.yaml` →
91
+ `spec.apps` (skip with `--no-stack-entry`) — no manual
92
+ `package.json` linking step. Check it with `mdk validate app <app-name>`.
93
+
94
+ ### 3. Author `mdk-plugin.json`
95
+
96
+ Every route needs:
97
+
98
+ - `id` — stable dotted id (`<domain>.<metric>`, e.g. `fleet.summary`)
99
+ - `handler` — relative path to the controller
100
+ - `http.method` + `http.path` — the UI will `fetch` this path
101
+ - `http.responses.200.content.application/json.schema` — real response shape
102
+ - `description`, `constraints`, `examples`, `errors`, `safety`
103
+
104
+ Path convention: `/api/<domain>/<resource>` (e.g. `/api/fleet/summary`). The Gateway serves it under
105
+ `/apps/<app-name>` (so `/apps/<app-name>/api/fleet/summary`) unless the app's `spec.apps` entry sets
106
+ `mount`; never write the `/apps/<app-name>` prefix into the route table yourself.
107
+
108
+ Full field notes: [`references/plugin-authoring.md`](./references/plugin-authoring.md).
109
+
110
+ ### 4. Implement the controller
111
+
112
+ Build the client once per app, in a `lib/client.js` every controller requires — the
113
+ scaffold's `plugin/lib/client.js` is this exact pattern
114
+ (`createMdkClient(config)` over `require('@tetherto/mdk-gateway/plugin')`).
115
+
116
+ ```js
117
+ 'use strict'
118
+
119
+ const mdkClient = require('../lib/client')
120
+
121
+ module.exports = async function myRoute (req) {
122
+ // mdkClient methods return the bare payload — never read `.payload`
123
+ const workersResp = await mdkClient.listWorkers()
124
+ const workers = (workersResp && workersResp.workers) || []
125
+ // … filter devices, pullTelemetry, shape response …
126
+ return { /* matches mdk-plugin.json schema */ }
127
+ }
128
+ ```
129
+
130
+ Typical [fan-out pattern](./references/controller-patterns.md):
131
+
132
+ 1. `listWorkers()` → collect `deviceIds`.
133
+ 2. `getCapabilities(deviceId)` → keep devices matching the contract fingerprint.
134
+ 3. `pullTelemetry(deviceId, 'metrics')` → read `metrics.<channel>`.
135
+ 4. Return a UI-friendly object (`unit`, totals, per-device rows).
136
+
137
+ ### 5. Register in `mdk.yaml`
138
+
139
+ Already done if you scaffolded with `mdk create app` (step 2). Otherwise
140
+ add it by hand (`name` matches `^[a-z][a-z0-9-]*$`):
141
+
142
+ ```yaml
143
+ spec:
144
+ apps:
145
+ - name: <app-name>
146
+ package: <app-name>
147
+ config: {}
148
+ # mount: / # only when a client calls the routes at the Gateway root
149
+ ```
150
+
151
+ `spec.gateway.plugins` is rejected by the CLI; move any such entry to `spec.apps`.
152
+ Then restart (`mdk run`, or `mdk run app <app-name>` beside `mdk run gateway`).
153
+ Wiring and boot order details live in `mdk-deployment`.
154
+
155
+ ### 6. Smoke-check the route
156
+
157
+ With the stack up:
158
+
159
+ ```bash
160
+ curl -s http://127.0.0.1:<gateway-port>/apps/<app-name>/api/<domain>/<resource> | jq .
161
+ ```
162
+
163
+ Confirm the JSON matches the schema and examples in `mdk-plugin.json`.
164
+ Only then build UI against it.
165
+
166
+ ## Non-negotiable invariants
167
+
168
+ - **Never reference a Worker, channel, or field** that is not in an installed
169
+ contract. Aggregation must be site-grounded.
170
+ - **`mdkClient` returns bare payloads** — do not unwrap `.payload`.
171
+ - **Recognize device families from capability signatures** when
172
+ `getCapabilities` omits brand/metadata (common). Prefer telemetry name sets
173
+ over hard-coded `deviceId` lists.
174
+ - Controllers are **read-only by default**; set `"safety": "read-only"` unless
175
+ the route intentionally mutates (commands).
176
+ - Throw `ERR_SCREAMING_SNAKE` strings; declare them under `errors` in the route.
177
+
178
+ ## Hand-off
179
+
180
+ | Next need | Skill |
181
+ | ------------------------------------- | ------------------------- |
182
+ | App manifest, permissions, packaging | `mdk-create-app` |
183
+ | Render this route in the dashboard | `mdk-ui-component` |
184
+ | Register / restart / verify the stack | `mdk-deployment` |
185
+ | Device does not exist yet | `mdk-worker-plugin` first |
186
+
187
+ ## References
188
+
189
+ - [`references/plugin-authoring.md`](./references/plugin-authoring.md): `mdk-plugin.json` fields + a fully populated route example
190
+ - [`references/controller-patterns.md`](./references/controller-patterns.md): list / filter / `pullTelemetry` patterns
191
+ - [`../mdk/references/protocol.md`](../mdk/references/protocol.md): envelope / action set
192
+ - Shipped monorepo examples (when present): `backend/apps/demo/` (read-only fan-out), `backend/apps/agent/` (auth + stream routes)
@@ -0,0 +1,99 @@
1
+ # Controller patterns
2
+
3
+ Build the client once per app, in a `lib/client.js` every controller requires. The
4
+ `plugin/lib/client.js` that `mdk create app` scaffolds is this exact pattern:
5
+
6
+ ```js
7
+ const { config } = require('@tetherto/mdk-gateway/plugin')
8
+ const { createMdkClient } = require('@tetherto/mdk-client')
9
+ module.exports = createMdkClient(config)
10
+ ```
11
+
12
+ `config` carries the app's own Kernel identity (`kernelKey`, `kernelKeyPair`), so the client connects with the app
13
+ runtime's key.
14
+
15
+ Controllers are plain async functions that require it:
16
+
17
+ ```js
18
+ const mdkClient = require('../lib/client')
19
+
20
+ module.exports = async function routeName (req) {
21
+ // ...
22
+ return payload
23
+ }
24
+ ```
25
+
26
+ `mdkClient` is `@tetherto/mdk-client`. Methods return **bare payloads** — never `.payload`. It connects on first use and
27
+ memoizes the connection; a failure maps to `ERR_MDK_CLIENT_UNAVAILABLE` and resets so the next call retries — no `null`
28
+ check needed, just a `try`/`catch` around the call if you want to remap the error.
29
+
30
+ ## Pattern: fan-out telemetry for a device family
31
+
32
+ ```js
33
+ const isTargetFamily = (capsPayload) => {
34
+ const telemetry =
35
+ (capsPayload && capsPayload.capabilities && capsPayload.capabilities.telemetry) ||
36
+ (capsPayload && capsPayload.telemetry) ||
37
+ []
38
+ const names = new Set(telemetry.map((t) => t && t.name).filter(Boolean))
39
+ // Fingerprint derived from the installed contract — adjust per family
40
+ return names.has('<channel-1>') && names.has('<channel-2>')
41
+ }
42
+
43
+ module.exports = async function example (req) {
44
+ const workersResp = await mdkClient.listWorkers()
45
+ const workers = (workersResp && workersResp.workers) || []
46
+ const deviceIds = workers.flatMap((w) => w.deviceIds || [])
47
+
48
+ const rows = []
49
+ let total = 0
50
+
51
+ for (const deviceId of deviceIds) {
52
+ const caps = await mdkClient.getCapabilities(deviceId).catch(() => null)
53
+ if (!isTargetFamily(caps)) continue
54
+
55
+ const tel = await mdkClient.pullTelemetry(deviceId, 'metrics').catch(() => null)
56
+ const metrics = (tel && tel.metrics) || {}
57
+ const value = Number(metrics['<channel-1>']) // channel name from contract
58
+ if (!Number.isFinite(value)) continue
59
+
60
+ rows.push({ deviceId, value })
61
+ total += value
62
+ }
63
+
64
+ return { unit: '<unit-from-contract>', total, devices: rows }
65
+ }
66
+ ```
67
+
68
+ ### Why capability fingerprints?
69
+
70
+ Kernel `getCapabilities` returns the capability list, not full
71
+ `mdk-contract.json` metadata (`brand`, `provider`, …). Matching on the
72
+ telemetry name set (and optionally command names) is the reliable filter.
73
+ Derive the set from the installed contract — do not guess.
74
+
75
+ ### Channel → metrics key
76
+
77
+ `pullTelemetry(deviceId, 'metrics')` returns `{ metrics: { <channel>: value, ... } }`.
78
+ Keys match contract telemetry `name` fields exactly.
79
+
80
+ ### Resilience
81
+
82
+ - `.catch(() => null)` per device so one offline device does not fail the route.
83
+ - Skip non-finite numbers instead of throwing.
84
+ - Empty `rows` is a valid 200 — the UI shows an empty state.
85
+
86
+ ## Pattern: single-device read
87
+
88
+ When the route takes `deviceId` (query/body):
89
+
90
+ 1. Validate `deviceId` is present.
91
+ 2. Optionally `getCapabilities` to confirm the channel exists.
92
+ 3. `pullTelemetry` once and return the shaped value.
93
+
94
+ ## Errors
95
+
96
+ | Error | When |
97
+ | --- | --- |
98
+ | `ERR_MDK_CLIENT_UNAVAILABLE` | `mdkClient` failed to connect (Kernel unreachable) |
99
+ | `ERR_<DOMAIN>_*` | Domain-specific; declare in `mdk-plugin.json` `errors` |
@@ -0,0 +1,121 @@
1
+ # mdk-plugin.json authoring
2
+
3
+ ## Package layout
4
+
5
+ ```
6
+ apps/<name>/
7
+ ├── package.json
8
+ ├── mdk-app.yaml
9
+ └── plugin/
10
+ ├── mdk-plugin.json
11
+ ├── lib/client.js
12
+ └── controllers/<route>.js
13
+ ```
14
+
15
+ ### package.json
16
+
17
+ ```json
18
+ {
19
+ "name": "<name>",
20
+ "version": "0.1.0",
21
+ "private": true,
22
+ "description": "<one-line: what telemetry/commands this aggregates>"
23
+ }
24
+ ```
25
+
26
+ `mdk create app <name>` sets this for you (bare `<name>`, or `@org/<name>`
27
+ with `--org`) — `mdk.yaml` references whatever name lands here, the same way
28
+ it would a published package. `mdk-app.yaml`'s `version` must equal this
29
+ `version`; the [`mdk-create-app`](../../mdk-create-app/SKILL.md) skill owns the rest of that manifest.
30
+
31
+ ### mdk-plugin.json — required route fields
32
+
33
+ | Field | Purpose |
34
+ | --- | --- |
35
+ | `name` / `version` / `description` | Package identity (often mirrors package.json) |
36
+ | `routes[].id` | Stable id (`<domain>.<resource>`) |
37
+ | `routes[].handler` | Relative path, e.g. `./controllers/power.js` |
38
+ | `routes[].auth` | `true` makes the Gateway answer `401` unless its `authVerifier` returns an identity, which reaches the controller as `req._info`. The Gateway hands `permissions` to that verifier as `route.permissions` and does not interpret them itself |
39
+ | `routes[].http.method` | `GET` / `POST` / … |
40
+ | `routes[].http.path` | Path the UI fetches (`/api/...`), served under `/apps/<app-name>` unless the app is mounted elsewhere |
41
+ | `routes[].http.responses` | Document `200` schema + error statuses |
42
+ | `routes[].description` | What the route does (operator + LLM context) |
43
+ | `routes[].constraints` | Runtime preconditions (Kernel connected, channels present) |
44
+ | `routes[].examples` | At least one request/response example with realistic values |
45
+ | `routes[].errors` | `ERR_*` strings the controller may throw |
46
+ | `routes[].safety` | `"read-only"` unless the route mutates devices. `mdk run mcp` withholds every other route unless started with `--allow-write` |
47
+
48
+ ## Response schema rules
49
+
50
+ - Schema properties must match what the controller **actually returns**.
51
+ - Prefer flat, UI-ready shapes:
52
+
53
+ ```json
54
+ {
55
+ "unit": "W",
56
+ "total": 3400,
57
+ "devices": [
58
+ { "deviceId": "dev-0", "value": 3400 }
59
+ ]
60
+ }
61
+ ```
62
+
63
+ - Include the contract `unit` as a field so the UI never hard-codes it.
64
+ - Name numeric fields with the unit suffix when helpful (`powerW`, not `power`
65
+ next to a separate `unit`), so cards can round without guessing.
66
+
67
+ ## Fully populated route example
68
+
69
+ Every field from the table above, together, on one route:
70
+
71
+ ```json
72
+ {
73
+ "id": "example.metric",
74
+ "handler": "./controllers/metric.js",
75
+ "auth": false,
76
+ "http": {
77
+ "method": "GET",
78
+ "path": "/api/example/metric",
79
+ "responses": {
80
+ "200": {
81
+ "description": "Aggregated metric across matching devices.",
82
+ "content": {
83
+ "application/json": {
84
+ "schema": {
85
+ "type": "object",
86
+ "properties": {
87
+ "unit": { "type": "string" },
88
+ "total": { "type": "number" },
89
+ "devices": { "type": "array" }
90
+ }
91
+ }
92
+ }
93
+ }
94
+ },
95
+ "400": { "description": "MDK client unavailable." }
96
+ }
97
+ },
98
+ "description": "Fans out telemetry.pull to matching devices and returns shaped rows.",
99
+ "constraints": [
100
+ "Requires the MDK client to be connected to a Kernel instance.",
101
+ "Devices must expose the contract channel(s) this route reads."
102
+ ],
103
+ "examples": [
104
+ {
105
+ "description": "Example success payload",
106
+ "request": "GET /api/example/metric",
107
+ "response": { "unit": "W", "total": 0, "devices": [] }
108
+ }
109
+ ],
110
+ "errors": ["ERR_MDK_CLIENT_UNAVAILABLE"],
111
+ "safety": "read-only"
112
+ }
113
+ ```
114
+
115
+ ## Registering
116
+
117
+ `mdk create app <name>` does all of this: links the package into the project's
118
+ root `package.json` (root `npm install`) and appends it under `mdk.yaml` →
119
+ `spec.apps[]` → `{ name: <name>, package: <name>, config: {} }`. Scaffolding by
120
+ hand instead, do the same two steps yourself, run `mdk validate app <name>`,
121
+ then restart with `mdk run`.