@dinglebear/unraid 0.2.5 → 0.6.2

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.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Rust MCP server and CLI for Unraid GraphQL operations across NAS, Docker, VM, and storage workflows.
4
4
 
5
- It exposes one MCP tool, `unraid`, plus the `runraid` CLI. Agents can inspect
5
+ It supports legacy and atomic MCP projections plus the `runraid` CLI. Legacy mode exposes the single `unraid` tool; atomic mode exposes one focused `unraid_<action>` tool per enabled action. Agents can inspect
6
6
  array health, disks, Docker containers and logs, VMs, shares, notifications,
7
7
  system metrics, UPS, logs, network settings, plugins, parity history, rclone,
8
8
  remote access, and Unraid Connect through stdio MCP, Streamable HTTP MCP, or
@@ -57,7 +57,7 @@ through MCP tool arguments.
57
57
  | crates.io package | `unraid-rmcp` |
58
58
  | CRGX command | `crgx unraid-rmcp -- <runraid args>` |
59
59
  | Legacy npm package | `unraid-rmcp` |
60
- | MCP tool | `unraid` |
60
+ | MCP tools | `unraid` (legacy), `unraid_<action>` (atomic) |
61
61
  | Config home | `~/.unraid` on hosts, `/data` in containers |
62
62
  | Env prefixes | `UNRAID_*`, `UNRAID_RMCP_*` |
63
63
 
@@ -120,7 +120,7 @@ cargo build --release
120
120
  ./target/release/runraid --help
121
121
  ```
122
122
 
123
- Minimum supported Rust version: 1.90.
123
+ Minimum supported Rust version: 1.97.1 (edition 2024).
124
124
 
125
125
  ## Quickstart
126
126
 
@@ -242,8 +242,13 @@ as action arguments.
242
242
 
243
243
  ## MCP Tool Reference
244
244
 
245
- One MCP tool is exposed: `unraid`. Pass the required `action` argument to select
246
- the operation.
245
+ MCP projection is configurable with `UNRAID_RMCP_PROJECTION` (or `mcp.projection`):
246
+
247
+ - `legacy` (default) exposes the compatibility tool `unraid`; pass its required `action` argument.
248
+ - `atomic` exposes one focused `unraid_<action>` tool for each enabled action. Atomic schemas contain only that action's parameters and mark dispatcher-required fields as required.
249
+ - `both` exposes the legacy tool plus all enabled atomic tools for migration. Generated prompts prefer atomic calls in this mode.
250
+
251
+ Selectors are applied to canonical actions before projection, so the same `[mcp.tools]` policy controls all three modes. Calls from either surface normalize to the same canonical action before scope checks, destructive confirmation, and dispatch.
247
252
 
248
253
  ### Core Actions
249
254
 
@@ -289,7 +294,7 @@ the operation.
289
294
  | `connect` | Unraid Connect dynamic remote access state. |
290
295
  | `plugins` | Installed community plugins with versions. |
291
296
  | `status` | Server observability: version, PID, uptime, and counters. |
292
- | `help` | Markdown reference for all actions. |
297
+ | `help` | Markdown reference for the actions enabled by server policy. |
293
298
 
294
299
  Pagination and filtering are MCP-only. List actions return a paginated envelope
295
300
  with `items`, `total`, `limit`, `offset`, `has_more`, and `next_offset`.
@@ -332,16 +337,24 @@ runraid setup repair [--json]
332
337
 
333
338
  ## Configuration
334
339
 
335
- Host installs read `~/.unraid/.env` before loading config. Containers read
336
- `/data/.env`. Process environment overrides both.
340
+ When `UNRAID_HOME` is non-empty, the binary reads `<UNRAID_HOME>/.env`.
341
+ Otherwise host installs read `~/.unraid/.env` and containers read `/data/.env`.
342
+ Non-empty process environment values override `.env`; empty plugin placeholders
343
+ inherit persisted `.env` values instead of clearing them. A present but malformed
344
+ `.env` is rejected so parsing cannot silently skip a later deny rule or credential.
337
345
 
338
346
  | Variable | Default | Purpose |
339
347
  |---|---|---|
340
348
  | `UNRAID_API_URL` | unset | Full Unraid GraphQL endpoint URL. |
341
349
  | `UNRAID_API_KEY` | unset | API key for the `x-api-key` header. |
342
350
  | `UNRAID_API_SKIP_TLS_VERIFY` | `false` | Skip TLS certificate verification for self-signed endpoints. |
351
+ | `UNRAID_API_CA_BUNDLE` | _(unset)_ | Path to a PEM CA bundle to trust. Prefer this over skipping verification for a private CA; startup fails if the file is unreadable or not a PEM bundle. |
352
+ | `UNRAID_HOME` | platform default | Exact data directory for `.env`, auth DB, and JWT key; overrides `/data` or `~/.unraid`. |
343
353
  | `UNRAID_RMCP_HOST` | `0.0.0.0` | HTTP bind host. |
344
354
  | `UNRAID_RMCP_PORT` | `40010` | HTTP bind port. |
355
+ | `UNRAID_RMCP_PROJECTION` | `legacy` | MCP tool projection: `legacy`, `atomic`, or `both`. |
356
+ | `UNRAID_RMCP_ENABLED_TOOLS` | unset | Comma-separated MCP tool/action allowlist; empty means all. |
357
+ | `UNRAID_RMCP_DISABLED_TOOLS` | unset | Comma-separated MCP tool/action denylist; deny rules win. |
345
358
  | `UNRAID_RMCP_SERVER_NAME` | `unraid-rmcp` | Advertised MCP server name. |
346
359
  | `UNRAID_RMCP_TOKEN` | unset | Static bearer token for HTTP MCP. |
347
360
  | `UNRAID_RMCP_NO_AUTH` | `false` | Disable auth only for loopback development. |
@@ -354,6 +367,18 @@ Host installs read `~/.unraid/.env` before loading config. Containers read
354
367
  | `UNRAID_RMCP_GOOGLE_CLIENT_ID` | unset | Google OAuth client ID. |
355
368
  | `UNRAID_RMCP_GOOGLE_CLIENT_SECRET` | unset | Google OAuth client secret. |
356
369
  | `UNRAID_RMCP_AUTH_ADMIN_EMAIL` | unset | Admin email for OAuth bootstrap. |
370
+ | `UNRAID_RMCP_AUTH_ALLOWED_REDIRECT_URIS` | unset | Comma-separated HTTPS redirect URI patterns allowed during OAuth dynamic client registration. Loopback HTTP callbacks are allowed automatically. |
371
+
372
+ Tool selectors may target the entire surface (`*`, `unraid`, or `unraid.*`) or a single canonical action (`docker_logs` or `unraid.vm_reset`). Selectors do not change between projection modes. Tool discovery and `unraid://schema/mcp-tool` reflect the active projection and expose only enabled actions and their parameters; disabled calls are still rejected when a client uses a stale cached schema. Atomic tool annotations derive read-only and destructive hints from the same canonical scope and elicitation metadata. Invalid selectors, projection values, and unknown TOML fields fail configuration loading.
373
+
374
+ A set, non-empty `UNRAID_RMCP_ENABLED_TOOLS` / `UNRAID_RMCP_DISABLED_TOOLS`
375
+ **replaces** the corresponding `[mcp.tools]` list from `config.toml` — env and
376
+ toml are never merged, so the env var is the complete policy for that list.
377
+ An env var set to the empty string is treated as a placeholder: a matching
378
+ value from `<UNRAID_HOME>/.env`, `~/.unraid/.env`, or `/data/.env` is loaded when
379
+ present, otherwise it behaves as unset. A non-empty value that parses to zero selectors (for example
380
+ `","`) fails startup. This policy governs the MCP surface only; the `runraid`
381
+ CLI is not filtered by `[mcp.tools]`.
357
382
 
358
383
  ## Authentication
359
384
 
@@ -370,7 +395,7 @@ HTTP MCP auth policy:
370
395
 
371
396
  Read actions require `unraid:read`; mutating actions require `unraid:admin`.
372
397
  Configured static bearer tokens are operator credentials and receive admin scope.
373
- OAuth clients receive the scopes granted by the authorization flow.
398
+ OAuth clients receive the scopes granted by the authorization flow. Hosted clients that use dynamic client registration must register an HTTPS callback present in `UNRAID_RMCP_AUTH_ALLOWED_REDIRECT_URIS`; loopback HTTP callbacks are accepted automatically. Prefer exact callback URLs, for example `https://grok.com/connectors/oauth/callback`, and use wildcard patterns only when the client requires them.
374
399
 
375
400
  ## Safety And Trust Model
376
401
 
@@ -414,8 +439,8 @@ CLI shim (src/cli.rs) argv -> service -> stdout
414
439
  - The npm package remains a compatibility surface only. Automatic npm publishing
415
440
  is enabled only when `NPM_TRUSTED_PUBLISHING_ENABLED=true`.
416
441
  - Docker/OCI metadata uses `ghcr.io/dinglebear-ai/unraid-rmcp:<version>`.
417
- - `agents/unraid-rs/.mcp.json` must launch `crgx unraid-rmcp -- mcp` so stdio
418
- clients resolve the crates.io package without requiring Node.
442
+ - `agents/unraid-rs/.mcp.json` must launch `npx -y @dinglebear/unraid mcp` so
443
+ stdio clients resolve the published npm package.
419
444
  - The root README is curated. `docs/INVENTORY.md` is the curated inventory for
420
445
  actions, CLI commands, env vars, HTTP endpoints, and dependencies.
421
446
 
@@ -431,18 +456,24 @@ npm --prefix packages/unraid-rmcp run check
431
456
 
432
457
  ## Verification
433
458
 
459
+ Run from `unraid-rs/`. The shared checker lives in this repository; no personal
460
+ checkout or external documentation script is required. The npm `check` command
461
+ also performs a packed-install smoke test and requires the supported Linux x64
462
+ platform; `npm --prefix packages/unraid-rmcp test` runs the portable wrapper tests.
463
+
434
464
  ```bash
435
- python3 /home/jmagar/workspace/soma/scripts/check-readme-guide.py README.md
465
+ python3 ../.github/scripts/check_documentation.py
436
466
  npm --prefix packages/unraid-rmcp run check
437
467
  cargo check
438
468
  cargo test
439
469
  git diff --check
440
470
  ```
441
471
 
442
- Runtime smoke:
472
+ Runtime smoke requires an explicitly selected disposable test server and
473
+ privately supplied credentials. It is not part of the offline documentation gate.
443
474
 
444
475
  ```bash
445
- UNRAID_API_URL=https://10-1-0-2.<hash>.myunraid.net:31337/graphql \
476
+ UNRAID_API_URL=https://your-test-server.example/graphql \
446
477
  UNRAID_API_KEY=... \
447
478
  runraid server --json
448
479
  ```
@@ -478,7 +509,7 @@ gateway.
478
509
  | Symptom | Check |
479
510
  |---|---|
480
511
  | `UNRAID_API_URL` or `UNRAID_API_KEY` is missing | Set it in env or `~/.unraid/.env`. |
481
- | TLS errors against Unraid | Set `UNRAID_API_SKIP_TLS_VERIFY=true` only for self-signed endpoints. |
512
+ | TLS errors against Unraid | Point `UNRAID_API_CA_BUNDLE` at your CA's PEM bundle to keep verification on. Use `UNRAID_API_SKIP_TLS_VERIFY=true` only when you have no bundle. |
482
513
  | HTTP `/mcp` returns unauthorized | Set `UNRAID_RMCP_TOKEN` and send `Authorization: Bearer <token>`. |
483
514
  | Stdio client hangs or logs JSON errors | Ensure client config runs `unraid-rmcp mcp`, not the default HTTP server mode. |
484
515
  | Large list response is truncated | Use `limit`, `offset`, `name`, or `state` filters on MCP list actions. |
package/lib/platform.js CHANGED
@@ -12,7 +12,7 @@ function binaryVersion() {
12
12
 
13
13
  function targetFor(platform = process.platform, arch = process.arch) {
14
14
  if (platform === "linux" && arch === "x64") {
15
- return { asset: "runraid-x86_64.tar.gz", binary: "runraid" };
15
+ return { asset: "runraid-x86_64.tar.gz", archiveBinary: "runraid-linux-x86_64", binary: "runraid" };
16
16
  }
17
17
  throw new Error(`Unsupported platform ${platform}/${arch}. Supported target: linux/x64.`);
18
18
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dinglebear/unraid",
3
- "version": "0.2.5",
3
+ "version": "0.6.2",
4
4
  "description": "Rust MCP server and CLI for Unraid GraphQL operations across NAS, Docker, VM, and storage workflows.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/dinglebear-ai/unraid#readme",
@@ -55,7 +55,7 @@
55
55
  "oauth"
56
56
  ],
57
57
  "mcpName": "ai.dinglebear/unraid",
58
- "binaryVersion": "0.2.5",
58
+ "binaryVersion": "0.6.2",
59
59
  "author": {
60
60
  "name": "dinglebear.ai",
61
61
  "url": "https://dinglebear.ai"
@@ -71,6 +71,18 @@ function extract(archive, destination) {
71
71
  const result = spawnSync("tar", ["-xzf", archive, "-C", destination], { encoding: "utf8" });
72
72
  if (result.status !== 0) throw new Error((result.stderr || result.stdout || "tar extraction failed").trim());
73
73
  }
74
+
75
+ function promoteExtractedBinary(target, destination, root = installRoot()) {
76
+ const extracted = path.join(root, target.archiveBinary || target.binary);
77
+ if (!fs.existsSync(extracted)) {
78
+ throw new Error(`archive did not contain expected binary ${path.basename(extracted)}`);
79
+ }
80
+ if (path.resolve(extracted) !== path.resolve(destination)) {
81
+ fs.rmSync(destination, { force: true });
82
+ fs.renameSync(extracted, destination);
83
+ }
84
+ fs.chmodSync(destination, 0o755);
85
+ }
74
86
  async function main() {
75
87
  if (process.env.UNRAID_RMCP_SKIP_DOWNLOAD === "1") { log("skipping binary download because UNRAID_RMCP_SKIP_DOWNLOAD=1"); return; }
76
88
  const target = targetFor();
@@ -84,8 +96,12 @@ async function main() {
84
96
  await download(url, archive);
85
97
  await verifyChecksum(url, archive);
86
98
  extract(archive, installRoot());
87
- fs.chmodSync(destination, 0o755);
99
+ promoteExtractedBinary(target, destination);
88
100
  log(`installed ${destination}`);
89
101
  } finally { fs.rmSync(tempDir, { recursive: true, force: true }); }
90
102
  }
91
- main().catch((error) => { log(error.message); process.exitCode = 1; });
103
+ if (require.main === module) {
104
+ main().catch((error) => { log(error.message); process.exitCode = 1; });
105
+ }
106
+
107
+ module.exports = { checksumFromText, promoteExtractedBinary };