-
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's connection and 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/1001/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-08-18T12:57:28.819Z",
"fix": null
}
}
]
}servers --json is that servers array on its own. Same objects, so code that
reads one reads the other.
| Field | |
|---|---|
state |
connected · idle · disabled
|
enabled |
not disabled in the config; the boolean behind the third state |
connected |
a live MCP session right now |
auth.kind |
oauth · headers (a static header in the config) · none (a stdio child) |
auth.state |
valid · refreshable · expired · unauthorized · n/a
|
auth.expiresAt |
ISO stamp when the access token lapses, or null when the server sends no expiry |
auth.fix |
the command a human has to run, or null while nothing is needed |
idle is the resting state, not a fault. Nothing has called that server
yet, or the idle sweep closed the connection after its TTL. 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(.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. Daemon
down means daemon.up: false and every server idle — which is the truth.
What this cannot tell you: a refresh token the provider has revoked still
reads as valid. Revocation is not observable locally, only on the next call.
Local state answers "is there a usable session on disk", not "will the server
accept it".
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.