-
Notifications
You must be signed in to change notification settings - Fork 0
Commands
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.
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 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.
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 versiontools 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.
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.
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.
| 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.
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.
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_PATReference 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.
| 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 |
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 notesNeither 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.
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.
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.
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.
Every command above operates on one instance, and MDUCT_PROFILE chooses it:
MDUCT_PROFILE=research mduct serversThat 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.