Skip to content

Commands

Kevin Straub edited this page Aug 5, 2026 · 4 revisions

Every command, its arguments and what it prints. mduct help is the same list in one screen; this page is the version with the edges in it.

Nothing here starts a daemon on purpose. Anything that needs one gets one — the first command that talks to a server autostarts it and blocks until it answers. index, hook run, config, status and doctor never do, which is why they are safe in a shell prompt or a session-start hook. servers --json is in that group too, unlike the text servers — a status poll must never be the thing that spawns a daemon.

Calling things

call <server> <tool> [args…]

Invoke one MCP tool. Arguments are key=value (scalar, coerced) or key:=<json> (anything typed); see Arguments & output for the full grammar.

Flag Effect
--args '<json>' merge an object of arguments; - reads it from stdin, @file from a file
--json print only the JSON payload, prose stripped — the flag that makes | jq work everywhere
--raw the full MCP envelope, unformatted
--compact / --no-compact minify (or don't) regardless of mduct config compact
--full print an oversized result anyway
--timeout <s> seconds; the IPC wait is extended past it automatically

Every flag takes --name value or --name=value. A token starting with - that is not a known flag is an error, never a tool argument — --timeout=5 used to arrive at the server as a field called --timeout, with the timeout silently dropped.

Exit codes: 0 success, 1 the call failed, 2 nothing was printed on purpose — either the oversized-list guard tripped (a long result the tool could offer a jq projection for; a big blob of prose is not one) or --json was asked of a result that has no JSON in it. A 2 is a decision, not an error: ask again, differently.

On an argument error the signature is printed after the message, so the retry does not need a mduct schema round trip.

run <tool> [args…]

Run a configured CLI tool with its stored env and argument wrapping. mduct run kubectl get pods is kubectl with the right KUBECONFIG and whatever flags the entry pins. Everything after the tool name is passed through untouched.

env <tool>

That tool's environment as shell exports, for a script that wants the library rather than the binary:

eval "$(mduct env playwright)"   # sets NODE_PATH at the installed version

tools <server> · schema <server> <tool>

tools lists names with argument signatures and one line of description, no schemas. It answers for CLI tools too, so an agent can ask about any capability without knowing which kind it is. schema prints one tool's full JSON schema, which is the thing you fetch once and then don't need again.

index [--refresh]

The capability block: one line per server, signatures where a server is small enough to carry them. Reads a cache the daemon fills as a side effect of use, so it works cold and without a daemon — --refresh fills that cache on purpose, which is what a fresh machine needs.

mcp

Serve the catalogue over MCP: tool names mirrored into an agent's namespace, no schemas, and calling one returns the mduct call … command instead of running it. Meant to be registered by hook install, not typed.

Servers and tools

Command What it does
servers configured servers with connection state; the instance path goes to stderr so stdout stays parseable
servers --json the same servers as data, including each one's auth state — see machine-readable state
add no arguments: an interactive picker (↑↓, / to search, to toggle)
add <name> -- <cmd…> a stdio server
add <name> --url <url> an http server
add <ref> [--as <name>] install from the public registry, version-pinned
add <name> --tool -- <cmd…> a CLI tool
search <query> search the public registry
import [<name>…] lift servers out of your Claude configs
remove <name> delete an entry
enable / disable <name> toggle without deleting

add takes --env K=V (repeatable), --note "text" and --replace; a --tool entry also takes --check <cmd> and --setup <cmd>. import takes --as <name> (one server at a time), --source <file> and --replace.

Write the note for someone choosing between servers with nothing else to go on. It is the line that ends up in every agent's context.

tool status · tool setup <name> · tool update [<name>]

Is it installed, install it, and bump a version-pinned npm tool to the latest. tool status also reports when a pin has fallen behind.

Secrets and OAuth

echo "$TOKEN" | mduct secret set GITLAB_PAT   # or omit the pipe for a hidden prompt
mduct secret list                             # names only, never values
mduct secret rm GITLAB_PAT

Reference a stored secret as ${GITLAB_PAT} anywhere in the config. Resolution prefers the process environment, then the store, so CI can override without editing anything.

auth <server> runs the OAuth sign-in for an http server. The token is stored next to the config and refreshed by the daemon; you do this once.

Daemon, hooks, diagnosis

Command What it does
status daemon up? plus the socket, config and secrets paths of the instance that answered
status --json the whole instance as one object: daemon, paths, and every server with its connection + auth state
logs [server] recent daemon activity, filtered to one server if named
shadow nudges against follow-up calls: did the redirect convert?
doctor servers attached directly to an agent that mduct already serves, with the token cost of the duplication
daemon run in the foreground — the way to read a startup failure
daemon --stop stop it; the next call starts a new one
daemon --install install a systemd user unit
config show the per-instance defaults
config compact on|off minify call output by default
config warnAbove <chars|off> the oversized-result guard
--version / -V the version, on stdout, exit 0
help / -h / --help this list; asking for help is not an error, so it exits 0

Machine-readable state

status --json and servers --json, for programs rather than people. Everything the text output shows, plus the one thing it never showed: whether a server's login still holds.

mduct status --json
{
  "version": "0.6.0",
  "profile": null,
  "daemon": { "up": true, "socket": "/run/user/1000/mduct.sock" },
  "config": "/home/you/.config/mduct/servers.jsonc",
  "secrets": "/home/you/.config/mduct/secrets.json",
  "servers": [
    {
      "name": "notes",
      "transport": "http",
      "state": "idle",
      "enabled": true,
      "connected": false,
      "note": "shared notes",
      "auth": {
        "kind": "oauth",
        "state": "valid",
        "expiresAt": "2026-09-01T10:00:00.000Z",
        "fix": null
      }
    }
  ]
}

servers --json is that servers array on its own. Same objects, so code that reads one reads the other.

Top level
version the mduct that answered
profile MDUCT_PROFILE echoed back, or null for the default instance
daemon.up the daemon answered. A daemon that replies with an error (one older than the CLI) is up; one that does not answer within 1.5s reads as down
daemon.socket config secrets the paths of the instance that answered
error present only when the config did not load, carrying the loader's message. The servers array is then empty — see a broken config
Per server
name the key from the config
transport stdio (a child process) · http (a remote)
state connected · idle · disabled
enabled not disabled in the config; the boolean behind the third state
connected a live MCP session right now
note the config's note, or null
auth.kind oauth · headers (a static header in the config) · none (nothing to authorize — a stdio child, or an http server with neither)
auth.state valid (tokens stored, not lapsed) · refreshable (lapsed, the daemon renews it) · expired (lapsed, nothing to renew from) · unauthorized (no usable token file) · n/a (kind is not oauth)
auth.expiresAt ISO stamp when the access token lapses. null whenever there is nothing to date: no OAuth, no tokens yet, or a server that sends no expiry
auth.fix the command a human has to run, or null. It ignores disabled — a server you turned off still reports its dead login, so filter on .enabled when you only want live ones

idle is the resting state, not a fault. Nothing has called that server yet, or the idle sweep closed the connection after its idleTtlMin (default 30 minutes). A dashboard that paints "not connected" red will report a healthy instance as broken.

refreshable needs nobody. The access token has lapsed but a refresh token is stored, so the daemon renews it on the next call. Only expired and unauthorized carry a fix, which is the whole point of the field:

mduct status --json | jq -r '.servers[] | select(.enabled and .auth.fix) | .auth.fix'
# mduct auth notes

Neither command starts the daemon. A status poll must never be the thing that spawns one, so both read the socket and give up if nothing answers — under 50 ms when nothing is listening, since the socket refuses immediately. Daemon down means daemon.up: false and every enabled server idle; a disabled one stays disabled.

Servers only. The config's tools section (kubectl, playwright, …) is not in this output. mduct tool status is their readout, and it has no --json — its check runs a subprocess per tool and reaches the npm registry, which is not something to poll.

What this cannot tell you: whether the server will actually accept the session. A refresh token the provider has revoked still reads as usable — valid while the access token is inside its lifetime, refreshable once it lapses, which is where a revoked session spends most of its time. Revocation is not observable locally, only on the next call.

A config that does not load is a state, not a crash. status --json reports it in error with an empty servers array and exits 0, so a poller can tell "this instance is misconfigured" from "mduct is not installed". servers --json exits 1 there instead — an empty array would claim no servers are configured.

Typos are silent: --jsn is not an error, it just isn't the flag, so you get the human readout at exit 0. The unknown-option guard covers call, not this. Output is always pretty-printed; pipe through jq -c . if you want it minified.

hook install claude [--settings <file>] [--remove]

Writes both hooks into ~/.claude/settings.json and registers the catalogue in ~/.claude.json. --settings points the whole operation at another installation, including the MCP file next to it.

hook install codex [--remove]

The same, as one marked block in ~/.codex/config.toml. It refuses rather than writing a file TOML would reject if [hooks] or [mcp_servers.mduct] already exist outside that block.

hook run session-start · hook run pre-tool-use

The hook bodies. They read a payload on stdin and are meant to be called by a harness, not by you — but running them by hand is a fair way to see what a hook would have said.

Instances

Every command above operates on one instance, and MDUCT_PROFILE chooses it:

MDUCT_PROFILE=research mduct servers

That is a separate config, separate secrets, separate OAuth tokens and a separate daemon under ~/.config/mduct-research/. No profile means ~/.config/mduct/. When something answers with the wrong servers, mduct status tells you which instance you actually reached — that is the first thing worth checking, and half of what looks like a bug.

Direct overrides still win over the profile: MDUCT_CONFIG, MDUCT_SECRETS, MDUCT_SOCKET, MDUCT_CACHE. Setting only some of them is how people end up talking to a daemon that holds a different config than the one they edited.

Clone this wiki locally