-
Notifications
You must be signed in to change notification settings - Fork 0
Configuration
One file: ~/.config/mduct/servers.jsonc, in JSONC so comments survive. Three
top-level keys, none of them required: servers, tools, defaults.
mduct add, mduct import and mduct remove rewrite this file. Your entries
survive, your comments do not. The file says so at the top.
A server is either a local process (command) or a remote endpoint (url). A
server entry with neither is rejected at load, with the file path in the
message. Setting both is not caught today: command wins and the url is ignored.
| Field | Type | Meaning |
|---|---|---|
command |
string | Binary to launch for a stdio server. Mutually exclusive with url. |
args |
string[] | Arguments for it. ${VAR} refs are expanded. |
env |
object | Extra environment for the child process. ${VAR} expanded. |
url |
string | HTTP endpoint of a remote MCP server. |
headers |
object | Sent with every request — where a bearer token goes. ${VAR} expanded. |
auth |
"oauth" |
Use the stored OAuth token for this server and refresh it automatically. |
guard |
object |
allow/deny tool patterns, enforced in the daemon. See below. |
shadow |
array | Nudge rules — see Shadowing. |
idleTtlMin |
number | Close the connection after this many idle minutes. Default 30. A busy connection is never swept. |
note |
string | One line shown in mduct servers and in the prompt index. Write it for whoever reads the index. |
disabled |
bool | Keep the entry, hide the server. |
"gitlab": {
"command": "npx",
"args": ["-y", "@yoda.digital/gitlab-mcp-server"],
"env": { "GITLAB_PERSONAL_ACCESS_TOKEN": "${GITLAB_PAT}" },
"guard": { "deny": ["delete_*"] },
"idleTtlMin": 60,
"note": "GitLab: MRs, pipelines, issues, repos"
},
"notes": {
"url": "https://mcp.example.com/mcp",
"auth": "oauth",
"note": "shared notes"
}"guard": { "allow": ["list_*", "get_*"], "deny": ["get_secret"] }Rules, in order:
-
denywins. A tool matching any deny pattern is refused, allow-list or not. - A present
allowlist is authoritative. Anything not matching is refused. - A missing
allowmeans everything is allowed. -
"allow": []therefore denies everything. That is deliberate: a guard fails closed. If you want "no restriction", leave the key out.
* is a wildcard anywhere in the pattern; everything else matches literally.
The guard runs in the daemon rather than in a prompt, so it applies equally to a human typing the command and to an agent that has talked itself into something.
Plain command-line programs. An agent gets one capability list instead of "MCP things" and "other things".
| Field | Type | Meaning |
|---|---|---|
run |
string | The binary. Required. |
args |
string[] | Prefix arguments, prepended to whatever the caller passes. |
env |
object | Environment for the run. ${VAR} expanded. |
check |
string | Shell command that exits 0 when the tool is installed — drives mduct tool status. |
setup |
string | Shell command that installs it — run by mduct tool setup <name>. |
note |
string | One line for the index. |
disabled |
bool | Hide it. |
"kubectl": {
"run": "kubectl",
"args": ["--insecure-skip-tls-verify=true"],
"env": { "KUBECONFIG": "${HOME}/.kube/test.yaml" },
"check": "kubectl version --client",
"note": "read-only cluster access"
}mduct run kubectl get pods -n defaultargs are a prefix, so the wrapping is applied in one place and cannot be
forgotten at a call site. For npm-backed tools pinned to a version
(bunx pkg@1.2.3), mduct tool status reports available updates and
mduct tool update bumps the pin.
| Field | Type | Meaning |
|---|---|---|
compact |
bool | Minify JSON output by default (same as passing --compact). |
warnAbove |
number | Warn when a result exceeds this many characters and suggest a jq projection instead of dumping it. --full bypasses. Off when unset. |
mduct config # show the resolved defaults
mduct config compact on
mduct config warnAbove 25000 # or: offwarnAbove is the seatbelt for the failure this tool exists to prevent: a call
that quietly dumps 40k characters into a context window and errors on nothing.
Any string field can hold ${NAME}. Resolution order:
-
process.env.NAME, so CI and one-off overrides need no ceremony - the 0600 secret store
echo "$TOKEN" | mduct secret set GITLAB_PAT # or a hidden TTY prompt
mduct secret list # names only, never values
mduct secret rm GITLAB_PATmduct add --env and mduct import move literal values into the store and
leave a ${ref} behind, so a token never lands in the config file.
| Variable | Effect |
|---|---|
MDUCT_PROFILE |
Named instance → ~/.config/mduct-<profile>/ with its own socket, secrets and daemon. |
MDUCT_CONFIG |
Config file path. Wins over the profile. |
MDUCT_SECRETS |
Secret store path. |
MDUCT_SOCKET |
Daemon socket path. |
MDUCT_CACHE |
Cache dir (default ~/.cache/mduct[-<profile>]). |
mduct status prints what each of these resolved to. It is the fastest answer
to "why is it not seeing my server".
// managed by mduct — edits survive, comments don't (rewritten on add/remove)
{
"servers": {
"gitlab": {
"command": "npx",
"args": ["-y", "@yoda.digital/gitlab-mcp-server"],
"env": { "GITLAB_PERSONAL_ACCESS_TOKEN": "${GITLAB_PAT}" },
"guard": { "deny": ["delete_*"] },
"note": "GitLab: MRs, pipelines, issues, repos"
},
"notes": { "url": "https://mcp.example.com/mcp", "auth": "oauth", "note": "shared notes" },
"old-thing": { "command": "node", "args": ["legacy.js"], "disabled": true }
},
"tools": {
"kubectl": { "run": "kubectl", "check": "kubectl version --client", "note": "cluster" },
"playwright": { "run": "bunx", "args": ["playwright@1.61.1"], "setup": "bunx playwright@1.61.1 install chromium", "note": "browser" }
},
"defaults": { "compact": true, "warnAbove": 25000 }
}
{ "servers": { /* MCP servers */ }, "tools": { /* plain CLIs */ }, "defaults": { "compact": true, "warnAbove": 25000 } }