Skip to content

Configuration

Kevin Straub edited this page Aug 1, 2026 · 9 revisions

One file: ~/.config/mduct/servers.jsonc, in JSONC so comments survive. Three top-level keys, none of them required: servers, tools, defaults.

{
  "servers": { /* MCP servers */ },
  "tools":   { /* plain CLIs  */ },
  "defaults": { "compact": true, "warnAbove": 25000 }
}

mduct add, mduct import and mduct remove rewrite this file. Your entries survive, your comments do not. The file says so at the top.

servers

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 currently caught, and url wins.

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

"guard": { "allow": ["list_*", "get_*"], "deny": ["get_secret"] }

Rules, in order:

  1. deny wins. A tool matching any deny pattern is refused, allow-list or not.
  2. A present allow list is authoritative. Anything not matching is refused.
  3. A missing allow means everything is allowed.
  4. "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.

tools

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 default

args 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.

defaults

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: off

warnAbove 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.

Secrets

Any string field can hold ${NAME}. Resolution order:

  1. process.env.NAME, so CI and one-off overrides need no ceremony
  2. 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_PAT

mduct 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.

Environment variables

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".

A whole file, for reference

// 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 }
}

Clone this wiki locally