Skip to content

Troubleshooting

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

Nothing happens / the call hangs

mduct status          # is the daemon up, and which instance answered?
mduct daemon --stop   # then run the next call — it restarts clean
mduct daemon          # foreground: the startup errors go to your terminal

Running the daemon in the foreground is the fastest way to see a server that fails to launch. A stdio server that writes to stdout at startup, or dies immediately, shows up there and nowhere else.

"unknown server" but it's right there in the config

You are probably talking to a different instance than you think.

mduct status
# config:  /home/you/.config/mduct-ci/servers.jsonc   ← MDUCT_PROFILE is set

MDUCT_PROFILE, MDUCT_CONFIG and an exported MDUCT_SOCKET all survive in a shell longer than anyone remembers setting them.

A server works from my shell but not from the agent

The daemon inherits the environment of whichever process first started it. If the agent starts it, the agent's environment is what the server sees.

Put what the server needs in the config (env), not in your shell profile:

"srv": { "command": "", "env": { "API_BASE": "https://internal.example.com" } }

Then mduct daemon --stop so the next call starts it fresh with the new config.

Config changes don't take effect

They should: the daemon watches the config file. When an entry changes, that server's connection is dropped so the next call reconnects with the new settings — and connections to unrelated servers are left alone on purpose.

So if an edit seems ignored, the file probably didn't parse:

mduct logs | tail -5
# config reload FAILED: server "srv": needs "command" (stdio) or "url" (http) — fix /home/you/.config/mduct/servers.jsonc

A config that fails to load is not applied — the daemon keeps running on the last good one rather than dropping every connection over a stray comma. That is the right behaviour and also why a broken edit can look like no edit at all. Check the logs before you go looking for a cache.

${VAR} stayed a literal

Resolution is process.env first, then the secret store. If neither has it, the value is left as written, which usually surfaces as an auth error from the server rather than an error from mduct.

mduct secret list      # names only — is it actually stored?

Note that the daemon's environment is not your current shell's environment (see above). The secret store exists precisely so this stops mattering.

OAuth stopped working

mduct auth <server>    # re-consent; the token is stored 0600 and refreshed automatically

A dead session fails with exactly that instruction. Tokens live next to the config, so a profile has its own.

Output isn't valid JSON

Use --json. Without it you get the server's text content, which for many servers is JSON wrapped in a sentence. With it you get the payload only.

If --json still gives you something odd, look at the envelope:

mduct call srv tool --raw

A tool is blocked and shouldn't be

mduct servers          # the guard is shown per server

Remember the precedence: deny beats allow, a present allow list is exhaustive, and "allow": [] blocks everything by design. Removing the key entirely is what "no restriction" looks like.

Parallel calls to one server are slow

They are serialised per server — one connection, one queue. That is deliberate: MCP sessions are stateful and interleaving requests on one is a good way to get confusing results. Parallelise across servers, not within one.

Two of everything after an import

mduct doctor           # servers attached directly AND served here

An MCP server attached directly to your agent still loads its full schemas. If mduct also serves it, you are paying twice and got nothing. Remove the direct attachment.

Clone this wiki locally