The shared core of the CLIs built on Thinger.io servers — thinger
(@thinger-io/cli) and
ThinRemote's thinr — and of the MCP servers they expose.
It holds everything that is not specific to a product:
- API client (
api) — axios with the active profile's server and token on every request, refresh on 401, self-signed TLS for localhost. - Auth (
auth) — username/password (mints a permanent API token), pasted token, OAuth device flow; interactive or fully non-interactive. - Profiles (
config) — several servers per CLI, selected by--profile,<PREFIX>_PROFILEor the saved default, and scoped per call (withActiveProfile) so concurrent MCP calls don't mix servers. In CI,<PREFIX>_SERVERand<PREFIX>_TOKENstand for a profile without any file. - REST helpers —
devices,property,resource,bucket,product,profile(product profiles),tokens,alarms,storage, with every account path built inpaths. - Generic commands (
cli,commands/*) — login, logout, profile, device, product, bucket, dashboard, mcp. - Dashboard apps (
dashboard,dashboard-init,dashboard-dev,dashboard-sdk) — create an app from a template of@thinger-io/dashboard-sdk, develop it on its own dev server (a link with a temporary token opens the local app against the server), and publish its build to a public storage that dashboards point at, with a warning when its SDK needs a newer server or is not the latest. - Documentation (
docs,docs/) — the guides for agents: the platform's concepts, product code and server administration (shipped here, marking what needs a given server version), the dashboard SDK's reference and types (from its package: the project's version, else the CLI's), and the CLI's own. - MCP (
mcp/*) — the stdio server and 25 generic tools: eightresource_*tools over a catalog of the server's resource types, and the operations that are not CRUD (bucket data, device resources, storage files, dashboard development and publishing, the documentation…), plus that documentation as MCP resources. - OpenAPI (
openapi) — the server's own API description, cached per server and version, with a bundled copy as fallback.
The core knows nothing about the product it runs for. A CLI defines its identity once, before anything else, and runs:
import { defineIdentity } from '@thinger-io/cli-core/identity';
import { runCli } from '@thinger-io/cli-core/cli';
defineIdentity({
name: 'acme', // binary name, used in hints
displayName: 'Acme IoT',
description: 'CLI for Acme IoT servers',
configDir: 'acme-cli', // ~/.config/acme-cli/config.json
envPrefix: 'ACME', // ACME_PROFILE, ACME_INSECURE, ACME_SERVER + ACME_TOKEN
token: { id: 'acme_cli', name: 'Acme CLI' }, // minted by the password login
oauth: { clientId: null }, // OAuth device-flow client, if the platform ships one
defaultServer: null, // suggested by the login prompt
mcp: { serverName: 'acme', toolPrefix: 'acme' }, // tools become acme_resource_list, …
productTemplate: {}, // optional defaults for new products
});
await runCli({ version: '1.0.0' });Each CLI keeps its own profiles, environment variables and MCP tool names, so two of them can run side by side without one operating on the other's servers.
runCli takes:
register(program, { device, product, bucket, dashboard })— add top-level commands, or subcommands inside the generic groups.onNoCommand(program)— what to do when called without arguments (default: help).mcpTools— the CLI's own MCP tools, declared with bare names (name: 'device_exec') and referring to others in descriptions as{tool:resource_list}or{family:storage_file}. Unknown references fail at startup. A tool flaggedreplaces: trueswaps out the generic tool of the same name.resourceHooks— the CLI's hooks on catalog types ({ product: { afterDelete } }), run after the core's: seemcp/resource-hooks.js.resourceTypes— resource types of the CLI's own platform, served by the sameresource_*tools as the server's: instead of a REST path each has functions for the verbs it supports (list,get,create,update,delete), itsparentandid, aschemaandnotesforresource_describe, andactionsforresource_call(on: 'item','collection'or'either'). The core checks the parent and id, and searches and pages whatlistreturns. SeeLocalTypeinmcp/tools-resource.js.resourceActions— actions the CLI adds to the server's types ({ device: { reboot: { description, run } } }), run withresource_calllike the server's. A type or action that clashes with the catalog fails at startup.mcpInstructions— server-level guidance sent to MCP clients.
Tool names follow <object>[_<subobject>]_<verb>, with the verbs list, get,
describe, create, update, delete, clone, call, read, write, send.
The server's collections are reached through eight generic tools —
resource_describe, resource_list, resource_get, resource_create,
resource_update, resource_delete, resource_clone, resource_call
(actions that are not CRUD) — that take a type
from the catalog (lib/mcp/catalog.js), a parent for child types and an
id. The catalog is a reviewed table, never discovered at run time: each
type has its path, API version, parent, id and the verbs it allows, with the
server's operationId for each (the tests check them against the bundled
OpenAPI document). resource_describe reads the request schemas and
examples from the OpenAPI document of the server the profile points at.
To draft catalog entries after an API change:
curl -s https://<server>/v1/server/swagger.json -o openapi/swagger.json
node scripts/catalog-draft.jsModules are imported by path: @thinger-io/cli-core/api,
@thinger-io/cli-core/mcp/registry, …
npm install
npm test # under a made-up "acme" identity
npm run lint
npm run typecheckMIT