-
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), and
exactly one of the two. Neither, or both, is rejected at load with the server
name and the file path in the message. Both used to load quietly, which meant a
half-finished migration ran the old local process while the config advertised a
remote endpoint.
| 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. |
maxConcurrent |
number | In-flight calls allowed at once. Default 1. Raise it only for a server you know is reentrant. |
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 } }