@thinremote/thinr-cli
v1.2.0
Published
CLI for ThinRemote — remote management for IoT devices, with an MCP server for AI clients
Maintainers
Readme
Thinr — ThinRemote CLI
A command-line tool for managing remote IoT devices on the ThinRemote platform, and an MCP server that lets AI assistants drive those devices programmatically.
Two operating modes:
- CLI — interactive or scripted device management from a shell.
- MCP server — a stdio Model Context Protocol server that exposes the same operations as typed tools for AI clients like Claude Code.
Both modes share the same auth, profiles, and device API layer, so you configure the tool once and use it in whichever mode fits the task at hand.
Table of contents
- Installation
- Initial setup
- Global options and environment variables
- Part 1 — CLI
- Part 2 — MCP server
- Profiles and multi-account use
- Development
- License
Installation
From NPM (recommended)
npm install -g @thinremote/thinr-cliFrom source
git clone https://github.com/Thin-Remote/thinr-cli.git
cd thinr-cli
npm install
npm linkRequires Node.js ≥ 18.
Initial setup
Run the CLI once with no arguments to authenticate:
thinrYou'll be prompted for the ThinRemote server, and can authenticate
with either username/password (OAuth 2.0 device flow) or a pre-issued
token. On success a profile is created at
~/.config/thinr-cli/config.json, named after the server hostname,
and marked as the default.
Every later command reads that profile automatically. Logging into a second server creates a second profile side-by-side; see Profiles and multi-account use.
Global options and environment variables
These apply to every subcommand:
| Flag | Env var | Meaning |
| ------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| --profile <name> | THINR_PROFILE | Use a non-default profile for this invocation. |
| -u, --user <name> | — | Admin impersonation: act on behalf of another user (requires admin privileges on the server). |
| — | THINR_INSECURE=1 | Accept self-signed TLS certificates globally. Localhost / 127.0.0.1 are always accepted; this flag extends the relaxation to any host, useful for LAN dev servers. |
Part 1 — CLI
All device- and product-scoped actions follow a subcommand-first
shape: the action comes before the target id, matching patterns like
kubectl describe pod <name> or git remote add <name> <url>.
thinr device <action> <deviceId> [args…] [options]
thinr product <action> <productId> [args…] [options]JSON output
Every data-producing command accepts -j, --json and writes a single
envelope to stdout so scripts can pipe it to jq:
{ "ok": true, "data": <payload> }
{ "ok": false, "error": { "message": "...", "code": "<code>" } }Process exit codes always reflect success (0) or failure (non-zero),
independently of the envelope.
Emitted error code values:
| code | meaning |
| ---------------- | ------------------------------------------------- |
| not_configured | CLI has no saved profile — run thinr to set up. |
| not_found | Device, property, resource or profile not found. |
| unauthorized | Token expired or insufficient permissions. |
| server_error | Non-success HTTP response from the server. |
| network_error | No response from the server. |
| input_error | Bad or missing CLI argument. |
| timeout | Command timed out on the device (exec). |
| cancelled | User interrupted with Ctrl+C (exec). |
| error | Fallback for anything uncategorised. |
Spinners and progress messages are suppressed in JSON mode so the
output is valid JSON without wrappers. Interactive commands
(console, tcp/tls/http) accept --json silently but
don't change their behaviour — they don't produce a discrete result.
exec buffers stdout/stderr when --json is set and emits a single
envelope on exit.
thinr device <action> [<deviceId>]
Most actions take a <deviceId> as their first positional. The list
action is the exception — it operates on the fleet, with an optional
<pattern> to filter results.
list
List every device visible to the active profile, or filter with a case-insensitive regex (matched against device id and name on the server side):
thinr device list # every device
thinr device list supermicro # only ids/names containing "supermicro"
thinr device list "^revpi|jetson" # regex alternation works too
thinr device list --json # JSON envelope with the raw API recordsCombine with the global -u, --user flag for admin impersonation.
console
Open an interactive terminal on the device:
thinr device console <deviceId>Proxies — tcp / tls / http
Create a TCP, TLS, or HTTP proxy to a remote endpoint reachable from the device:
thinr device tcp <deviceId> [target] [options]
thinr device tls <deviceId> [target] [options]
thinr device http <deviceId> [target] [options]target is the remote address and port on the device side. Defaults
when omitted: tcp → 22, tls → 443, http → 80, against
localhost.
Options:
-p, --port <port>: Local port to bind (default: random in 50000–51000)--no-open: Do not open the browser automatically (http only)
Examples:
# TCP proxy to SSH
thinr device tcp RevPi20679 22
# HTTP proxy to a specific port
thinr device http RevPi20679 8080
# HTTP proxy to a specific address behind the device
thinr device http RevPi20679 http://192.168.1.45:8080status
thinr device status <deviceId> [-j, --json]Returns the device's connection stats (uptime, tx/rx, last-seen).
property
List every property of a device, or read one by id:
thinr device property <deviceId> # list
thinr device property <deviceId> <propertyId> [options] # readOptions:
-j, --json: Output as JSON-f, --field <path>: Extract a sub-field via dot path (-f data.value)
resource
List the resources exposed by the device (with their in/out
schemas when advertised), or call one:
thinr device resource <deviceId> # list
thinr device resource <deviceId> <resourceId> [options] # callOptions:
-i, --input <key=value>: Resource input (repeatable)-j, --json: Output as JSON-f, --field <path>: Extract a sub-field from the result (dot path)
exec
Run a shell command on the device and stream stdout/stderr back:
thinr device exec <deviceId> "<command>" [options]Options:
-j, --json: Buffer output and emit a single envelope{stdout, stderr, exitCode}on exit--legacy: Use the non-streaming one-shot API (older agents)
The process exits with the remote command's exit code.
update
Check for or apply an agent update on the device:
thinr device update check <deviceId> [options]
thinr device update apply <deviceId> [options]Options:
--channel <name>: Update channel (default:latest)-j, --json: Output as JSON
thinr product <action> <productId>
Fan out an operation across every device that belongs to a product.
property
thinr product property <productId> <propertyId> [options]Options:
-j, --json: Output as JSON. Emits one envelope with aresults[]array, one entry per device, each with its ownokflag.-f, --field <path>: Extract a sub-field from each property (dot path)-a, --all: Include offline devices (default: only active)-g, --group <group>: Filter devices by asset group
resource
thinr product resource <productId> <resource> [options]Options:
-j, --json: Output as JSON (results[]array, same shape as above)-f, --field <path>: Extract a sub-field from each result (dot path)-g, --group <group>: Filter devices by asset group-i, --input <key=value>: Resource input (repeatable)
Offline devices are skipped for resource — the server would reject
the call anyway.
exec
Run a shell command in parallel across every active device of the product and report a per-device status table plus each device's output:
thinr product exec <productId> <command...> [options]Options:
-j, --json: Output as JSON. Emits a single envelope with asummaryblock and aresults[]array carrying stdout, stderr, exit code,timedOutflag and per-device duration.-c, --concurrency <n>: Max parallel executions (default: 10)--timeout <seconds>: Per-device timeout (default: 30)--fail-fast: Stop dequeueing new devices as soon as one fails-g, --group <group>: Filter devices by asset group-a, --all: Include offline devices (default: only active)
Examples:
# hostname on every active device
thinr product exec my-product hostname
# apt upgrade across the fleet with 5 parallel slots and a long timeout
thinr product exec my-product "apt-get -y upgrade" -c 5 --timeout 600
# target only a subset of the product
thinr product exec my-product "systemctl restart agent" -g stagingAlso exposed as an MCP tool (thinr_product_exec) with the same
parameters, so agents can drive a fleet operation with a single call
instead of looping thinr_exec per device.
playbook
Manage playbooks stored on a product and run them against one device or the whole fleet:
thinr product playbook list <productId> [--json]
thinr product playbook upload <productId> <name> [file] [--description <text>] [--skip-validation] [--json]
thinr product playbook download <productId> <name> [-o <file>] [--json]
thinr product playbook delete <productId> <name> [--json]
thinr product playbook run <productId> <name> (--device <id> | --fleet) [options]upload reads the YAML from a file, or from stdin when the path is
omitted or -. The document is validated against the playbook schema
before being written to the product storage; an invalid document is
rejected with a descriptive error and nothing is uploaded. Pass
--skip-validation only to work around a schema-runtime gap (do not
use it to bypass real errors).
run targets either a single device (--device <id>) or the whole
fleet (--fleet, progressive batch rollout):
-v, --var <key=value>— override an overridable playbook variable (repeatable; values are coerced to the declared type).--vars-file <path>— load variable overrides from a YAML/JSON file.--dry-run— print the resolved plan without contacting any device.--check— contact devices read-only and report what each step would change.--yes/-y— skip the interactive confirmation prompt.
Fleet-only options:
--batch-size <n>— devices attempted in parallel per batch (default: 5).--failure-threshold <p>— abort rollout when cumulative failure rate reaches P% (default: 25).-g, --group <id>— restrict to an asset group.--filter <key=value>— extra server-side filter (repeatable).--include-offline— attempt offline devices too (default: active only).
Each fleet rollout writes a persistent JSON report to
playbooks/runs/<timestamp>-<name>-<user>.json in the product's file
storage, capturing resolved variables, per-device outcomes, and any
abort reason.
thinr playbook <action>
Run or validate a local playbook YAML file (not stored on a product). Useful for one-shot or ad-hoc runs:
thinr playbook validate <file>
thinr playbook run <file> [--dry-run] [--check] [options]validate parses the YAML and reports any schema errors. run
resolves the target block declared inside the playbook and executes
it; the same variable-override flags as thinr product playbook run
apply. For playbooks that should be reusable across users, upload
them to their product with thinr product playbook upload instead.
thinr profile
Manage the profile store (see Profiles and multi-account use):
thinr profile list [--json]
thinr profile current [--json]
thinr profile use <name> [--json]
thinr profile delete <name> [--json]thinr logout
Remove the active profile's credentials and config:
thinr logoutOther profiles are left untouched.
Part 2 — MCP server
thinr ships an MCP server that exposes the CLI's device
operations as a set of typed tools. AI clients (Claude Code, Claude
Desktop, and any other MCP-compatible host) can then list devices,
run shell commands, read/write files, call resources, and manage
products through the same API the CLI uses — with stable schemas and
a consistent error contract.
Starting the server
thinr mcp [-d, --device <deviceId>]The server speaks MCP over stdio. -d/--device sets a
default device that tools can omit in each call; --user can be
passed through (via the global flag) to impersonate another account.
Tool catalog
Grouped by capability. Every tool accepts optional device, user,
and profile arguments, so a single session can target any
device/account/environment without restart.
| Area | Tools |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Discovery | thinr_devices (with optional query for regex filtering), thinr_device_info, thinr_profiles |
| Shell | thinr_exec (buffered), with streaming stdout/stderr |
| Filesystem | thinr_read, thinr_write, thinr_push, thinr_pull, thinr_ls, thinr_mkdir, thinr_rm, thinr_mv |
| Resources | thinr_resource_list (with in/out schemas), thinr_resource_call |
| Properties | thinr_property_get, thinr_property_set |
| Scripts (device) | thinr_script_list, thinr_script_write, thinr_script_delete |
| Monitoring and update | thinr_monitoring, thinr_bucket_read, thinr_update |
| Products | thinr_products, thinr_product_delete, thinr_device_set_product, thinr_product_exec, thinr_product_write |
| Product scripts | thinr_product_script_list, thinr_product_script_read, thinr_product_script_write, thinr_product_script_delete |
| Playbooks (authoring) | thinr_playbook_schema, thinr_playbook_validate, thinr_playbook_run (ad-hoc) |
| Product playbooks | thinr_product_playbook_list, thinr_product_playbook_read, thinr_product_playbook_write, thinr_product_playbook_run (single device), thinr_product_playbook_rollout (fleet), thinr_product_playbook_delete |
Full input/output schemas are published via standard MCP
list_tools; the client will show them when you connect.
Authoring playbooks from natural language
Playbooks are declarative YAML documents — steps like exec, write,
install_package, service, plus a target block and a vars
block — that the runner applies to a device or a fleet. With the MCP
server connected, an AI assistant can turn a natural-language request
into a validated playbook and store it on the relevant product
without the user ever writing YAML by hand.
The intended authoring loop is:
- Fetch the schema. Call
thinr_playbook_schemaonce at the start of the session to get the authoritative list of top-level fields, target block, step actions with their parameters, and the two accepted forms of thevarsblock (plain and extended). - Draft the YAML. Translate the user's request into a playbook
draft. Use the extended
varsform (default/description/type/overridable/required) whenever the user hints at knobs the caller may need to tune at run time. - Validate and iterate. Call
thinr_playbook_validatewith the draft. On failure, read the consolidated error message, fix the YAML, and call validate again. Never skip this step. - Upload. Once validation is clean, call
thinr_product_playbook_writewith the product id, a short name (letters / digits / underscore / dash), and the validated YAML. The tool re-validates server-side; never passskip_validationto mask a real error. - Pre-flight on one device. Call
thinr_product_playbook_runwith a singledevice(plusdry_run: trueorcheck: truefor a read-only preview) to confirm the steps behave as expected. - Roll out. Finally,
thinr_product_playbook_rolloutrolls the playbook across the fleet in batches with a failure-rate kill switch, and writes a persistent report toplaybooks/runs/in the product file storage.
Example prompt the user can give the assistant:
"On product
edge-gateways, create a playbook callednginx-upgradethat updates the nginx package, renders/etc/nginx/nginx.conffrom a template, and restarts nginx only if the config check passes. Makeworker_connectionsan overridable integer variable, default 1024. Then try it onedge-gw-17in check mode."
A well-behaved assistant translates this into:
thinr_playbook_schema→ read the authoring contract.- Draft YAML →
thinr_playbook_validateuntil clean. thinr_product_playbook_writewithproduct="edge-gateways",name="nginx-upgrade", and the validated content.thinr_product_playbook_runwithdevice="edge-gw-17"andcheck: trueto preview what would change.
After that a second message like "looks good, roll it out to staging"
maps to thinr_product_playbook_rollout with the matching
group / filter and a conservative batch_size.
Per-call controls
The server starts with no per-session pin: every tool call must say which device it's for, and may optionally override the account or profile for that single call.
device— required for any device-scoped tool. Usethinr_devices(optionally withqueryto filter by regex) to discover ids.user— optional admin impersonation; falls back to the authenticated user of the active profile.profile— optional. Switches the active profile for that single tool call so an agent can hop between prod and staging without restarting the server. List the configured profiles withthinr_profiles.
Integrating with Claude Code
Register the server once with the Claude Code CLI:
claude mcp add thinr -s user -- thinr mcpThat's it — a single MCP server entry handles every device of the
active profile. Tool calls pass device (and optionally user /
profile) per call.
Other MCP hosts follow the same pattern — they just need the command
thinr mcp (optionally with -d <deviceId>) over stdio.
Profiles and multi-account use
~/.config/thinr-cli/config.json stores any number of profiles:
{
"default": "perf.aws.thinger.io",
"profiles": {
"perf.aws.thinger.io": { "server": "...", "username": "...", "token": "..." },
"staging.local": { "server": "...", "username": "...", "token": "..." }
}
}The active profile is picked, in order:
--profile <name>flag on the command line.THINR_PROFILEenvironment variable.- The
defaultfield in the config file. - The sole profile, if there's only one.
Logging in to a new server creates a new profile whose name is the server hostname (so it never clobbers an existing one). Legacy single-profile files from older versions of the CLI are detected on read and migrated in-memory without a manual step.
Use thinr profile use <name> to change the persisted default, or
--profile <name> / THINR_PROFILE=<name> for one-off runs.
Development
thinr-cli/
├── bin/ # CLI entry point (thinr)
├── commands/ # Commander subcommand modules
├── lib/ # Core: api, auth, config, device-api, errors,
│ # output, mcp-server, proxy, console, …
├── package.json
└── README.mdLocal development:
npm install
npm link # makes `thinr` available on $PATH
node bin/thinr.js # or run directly without linkingUnit tests live under tests/ and run offline:
npm testIntegration tests
The product-playbook end-to-end script at
tests/integration/playbook-e2e.js
drives a real ThinRemote cloud through the full playbook lifecycle
(create product → upload flat/extended/internal playbooks → list →
download → replace → reject invalid inputs → optionally run in
check mode against a device → delete). Cleanup of the temporary
product and its file storage is unconditional (try / finally),
even if the run fails halfway.
Prerequisites:
- An authenticated CLI profile (
thinronce, orTHINR_PROFILE=<name>pointing at an existing entry in~/.config/thinr-cli/config.json). - The account must be allowed to create products and file storages.
Invocations:
# Offline checks only — parses fixtures, exercises resolveVarScope,
# asserts the dry-run plan. No network access. Safe in CI without
# credentials.
node tests/integration/playbook-e2e.js --offline
# Full flow against the active profile. Creates a throw-away product
# named "playbook-e2e-<timestamp>-<rand>" and deletes it on exit.
node tests/integration/playbook-e2e.js
# Add a test device to exercise `check`-mode execution with and
# without variable overrides. The device must belong to the active
# user; it does NOT need to be bound to the temporary product.
THINR_E2E_DEVICE=<device-id> node tests/integration/playbook-e2e.jsThe script only touches resources it creates itself (identified by
the playbook-e2e- product-id prefix). If a run is interrupted
before the finally cleanup executes — e.g. the process is killed
— list leftovers with thinr product list --json and drop each one
through the MCP thinr_product_delete tool (from an AI client) or
via a Node one-liner reusing deleteProductWithStorage from
lib/product.js.
No credentials or tokens are ever written to the repository; the script reads them from the standard CLI profile store.
License
Released under the MIT License.
Copyright © Thinger.io
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
