
mduct
by TheFox666
One binary. Any number of MCP servers and plain CLIs behind a single command, with none of their tool schemas anywhere near your model's context.
mduct call gitlab list_issues state=opened --json | jq '.[].title'
That's the whole idea. It is a duct. Things go through it.
Why
An MCP client loads every connected server's tool schemas up front, all of them. Measured on one GitLab server: 186 tools, about 168 kB of JSON Schema. That is roughly 40k tokens spent before the model has read your question, paid again on every context refresh, mostly for tools nobody calls.
mduct puts one line per server in the prompt and leaves the schemas on disk until something actually calls a tool:
MCP tools via `mduct` (list: mduct tools <server>; call: mduct call <server> <tool> key=value):
gitlab — GitLab: MRs, pipelines, issues, repos
kubectl — read-only cluster access
One line is about 20 tokens. A full index of seven servers and three CLI tools measures 789 bytes. The tool stays discoverable, it just stops being expensive.
That last part is why the index exists at all instead of loading schemas lazily. Lazy loading fixes the token bill and introduces a worse problem: out of context, out of mind. An agent will not use a capability it cannot see.
What you get
| Warm daemon | Connections and OAuth sessions survive between calls. A stdio server isn't respawned and a remote isn't re-handshaked every time you invoke it. |
| Pipe-ready output | --json strips the prose some servers wrap around their payload, so | jq works. --compact minifies. Exit codes mean what you think they mean. |
| Guards in the daemon | Per-server allow/deny patterns, living somewhere the model cannot argue with them. |
| Secrets out of the config | ${VAR} refs resolve from a 0600 store. Plaintext tokens never touch servers.jsonc. |
| MCP and plain CLIs | kubectl, playwright and friends show up in the same list and are called the same way. Nobody has to care which is which. |
| Isolated instances | One env var gives a second agent its own config, secrets, auth and daemon. |
Install
curl -fsSL https://raw.githubusercontent.com/TheFox666/mduct/main/install.sh | sh
Fetches the release binary, checks its sha256, puts mduct in ~/.local/bin.
From a checkout: bun run build && cp dist/mduct ~/.local/bin/.
Quickstart
Stash a token. ${VAR} refs resolve from a 0600 store, so it never reaches
the config file:
echo "$YOUR_GITLAB_PAT" | mduct secret set GITLAB_PAT
Declare a server in ~/.config/mduct/servers.jsonc:
{
"servers": {
// local: mduct launches the process and talks stdio
"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"
},
// remote: nothing to install, mduct speaks HTTP to it
"notes": { "url": "https://mcp.example.com/mcp", "auth": "oauth" }
}
}
You don't have to hand-edit it. mduct add opens a picker and mduct import
lifts servers out of an existing Claude config.
Call things. The daemon autostarts:
mduct servers # what's configured
mduct tools gitlab # tool names + signatures, no schemas
mduct schema gitlab create_issue
mduct call gitlab create_issue project=42 title="it broke again"
mduct status # which instance answered, and from where
Wire an agent to it. Claude Code has hooks for this. Anything else: paste
mduct index into the system prompt.
mduct hook install claude
How it works
flowchart TD
A["you / your agent<br/><code>mduct call gitlab list_issues</code>"] -->|unix socket| D
D["daemon
Related servers

n8n
by n8n-io
Fair-code workflow automation platform with native AI capabilities. Combine visual building with custom code, self-host or cloud, 400+ integrations.

@modelcontextprotocol/server-everything
OfficialMCP server that exercises all the features of the MCP protocol

@modelcontextprotocol/server-filesystem
OfficialMCP server for filesystem access