If you've wired MCP servers into an agent, you've probably got a config file listing a dozen of them and a UI that shows a green dot next to each. Some of those dots are lying. A connector nobody has called in a month and a connector whose refresh token died three weeks ago look exactly the same from the outside: nothing has called either one. You find out which is which when the agent reaches for it mid-task and gets a 401. By then the agent has usually made up a reason for the 401 as well.
I spent five months on this problem while building Vodou, a local-first system that keeps your memory on your machine and routes work across dozens of MCP servers. Most of what I learned applies to any MCP host.
An idle connector and a dead one return the same nothing
The capability has two parts. The first is the catalog. It covers any stdio server you point at, any HTTP server, and a set of app presets, each of which is a JSON file describing one provider and one auth path. Four paths exist, tried in order of preference: a local subprocess, Dynamic Client Registration (you click Connect, authorize in a popup, and the tokens land on your machine), a pasted API key, and manual OAuth as the fallback. The presets are public (the contributor rules are in docs/vodou-apps.md), so adding a provider means opening a PR against a JSON file. You never touch gateway code.
The second part is what makes the catalog trustworthy: a connection ledger. For every connector it answers one question: if the agent reaches for this right now, will it work? It answers with one of eight states, and each state comes with a reason written as a sentence. The two that matter most are idle-verified (nothing called it for 30 days, but the weekly probe answered) and expired-reconnect (refresh failed five times in a row, so sign in again). Treating those two as the same is how a dead connector sits unnoticed for weeks while a healthy unused one nags you.
Where a connector's state comes from
The Mac server shipped, ran, and never showed up on the Apps page
The first failure had nothing to do with auth. In early May, the macOS automation server was in the release build and started by the service script on every boot, and it still never appeared as something a person could connect. The reason was that there was no preset file for it. The process registry and the catalog were two separate lists, and I had only updated one of them. The fix was a preset with six setup steps. The Accessibility permission step alone needed a warning that the macOS "+" picker sometimes won't show the binary. It also taught me that "running" and "connectable" are separate facts, and a host has to know both.
A healthy server registered against a deleted /tmp node binary
Next came a server that "failed" on every call even though it was fine. Its registration row pointed at a Node binary and an entrypoint under a /tmp/...connect-test-4628/ directory left over from an install test. The OS had since cleaned up that directory. When I ran the server by hand it created a session, stored all five test thoughts and computed its quality score correctly. The only thing broken was the path saved in the database. Re-registering it with a portable relative command (./.node/node plus the server's dist/index.js) fixed it. I'd seen a similar gap before: nothing checked that the saved command could still run.
31 refresh attempts on a credential that could never recover
The ledger has a ceiling on refresh attempts because I needed one. On the reference install, one OAuth credential kept trying to refresh with no end. It made 31 attempts, and every one was refused, because the grant behind it had been revoked. Each failure was logged, and nothing moved the state from "refreshing" to "you need to sign in again". Now five consecutive failures ends the retrying and the row reads expired-reconnect.
Refresh attempts before anyone was told
--headers was parsed, validated, and dropped before the wire
This one is the most embarrassing. The CLI connect command accepted --headers for remote servers that authenticate with a static header instead of OAuth. It parsed the flag and validated it, then built the HTTP connection with no headers at all, on both connect paths. So a header-authenticated server couldn't be connected from the CLI. The 401 it returned was reported as "OAuth configuration discovered", which sent you off to the credential store to fix something the flag was supposed to handle. --validate made it worse by looking safer: its probe sent no credentials, so validation always failed against exactly the servers that needed the flag.
Invariant: a connector's state is computed from evidence, and configured auth reaches every request path
There are two properties here, and you can check both against a codebase. First: a connector's displayed state must be derived from its credential and its most recent successful call, and a stored health field may only be compared against that result, never returned in its place. Second: every code path that opens a connection (add, validate, reconnect, background probe) must send the same auth material, and you can prove that from the wire, not from reading the code. All four failures above break one of the two.
Point your connect paths at a listener and grep for the canary
You can run this in five minutes, with no Vodou involved. Start a fake endpoint that logs whatever your client actually sends:
# echo_auth.py: logs the Authorization header each request carries
from http.server import BaseHTTPRequestHandler, HTTPServer
class H(BaseHTTPRequestHandler):
def do_POST(self):
print(self.command, self.path, "auth=", self.headers.get("Authorization", "<MISSING>"), flush=True)
self.send_response(401); self.end_headers()
do_GET = do_POST
HTTPServer(("127.0.0.1", 9911), H).serve_forever()
Register http://127.0.0.1:9911/mcp in your host with the header Authorization: Bearer canary-123. Then trigger every path you have: the first connect, any "test connection" button, a restart, and a scheduled health probe. Pass: every logged line shows auth= Bearer canary-123. Fail: any line shows auth= <MISSING>. Also check what your UI said about the 401. If it said anything except "unauthorized", your error mapping has the same bug mine did.
Next, check for registrations that point at files that no longer exist:
jq -r '.mcpServers | to_entries[] | select(.value.command) | [.key, .value.command, (.value.args[0] // "")] | @tsv' mcp.json |
while IFS=$'\t' read -r name cmd arg; do
command -v "$cmd" >/dev/null 2>&1 || echo "DEAD $name: $cmd"
case "$arg" in /*) [ -e "$arg" ] || echo "DEAD $name: $arg";; esac
done
No output means pass. Any DEAD line is a server that will "fail" even if the server itself is fine.
Last, if you log tool calls, compare the stored health word against the evidence (Postgres shown):
SELECT c.name, c.health AS stored,
max(t.called_at) FILTER (WHERE t.ok) AS last_ok
FROM connectors c LEFT JOIN tool_calls t ON t.connector = c.name
GROUP BY c.name, c.health
HAVING c.health = 'healthy'
AND coalesce(max(t.called_at) FILTER (WHERE t.ok), 'epoch') < now() - interval '30 days';
Every row returned is a connector your UI calls healthy with no evidence behind it.
MCP security guides assume the header you configured is the header you sent
The published advice on this is good on policy. The Cloud Security Alliance guide says remote connections should use OAuth 2.1 and warns against trust-on-first-use defaults. AWS's MCP guidance makes governance one of its three pillars. Seekvana's walkthrough makes the useful point that "the tool isn't there" is always a specific hop you can check. What none of them cover is time. They describe how to set up a connection, not how to know three weeks later whether it still works. They also don't consider a client that accepts auth config and then quietly fails to send it. A policy is only as good as the request that actually goes out.
Still open: the probe proves an answer, not a working tool
idle-verified means the weekly probe got a response. It doesn't mean the tool you need will accept your arguments today, because a probe can't check that. The headers fix is written and verified against a real header-authenticated remote, but at the time of writing it has not merged, so the CLI path for those servers is still broken on the main branch.
Your memory decides which of those connectors gets called
The ledger exists because I couldn't trust my own tray of green dots, and an agent working on my behalf was trusting them more than I was. In Vodou, connections sit under something I care about more: your memory lives in a local database you own, and it travels with you into ChatGPT, Claude and Gemini in the browser as well as Claude Code, Cursor and VS Code. When a request routes to one of your connected servers, the routing and the context behind it come from your machine.
In practice that looks like this. You connect a Notion or Linear preset in one click, and its tools are available in chat, in the CLI, in scheduled tasks and in automations, with no restart. A scheduler runs those tools while you're away. This blog is one example: it's mined from memory, drafted, graded, scanned by a redaction gate and deployed on that scheduler. Proactive loops tell you when an automation stopped or capture went quiet, so an expired-reconnect connector gets to you before your next task does. Anything outward-facing waits for your approval, and there's an audit trail of what ran. Every part can be extended: presets, skills, MCP servers and schedules are all yours to add and change.
Vodou is for engineers who already run several AIs and several MCP servers, and who want one local system that remembers them and tells the truth about what's connected.
If you want connectors graded from real evidence, with the memory behind them staying on your own machine, start at vodou.ai.
Source: Your MCP client can't tell a dead connector from an idle one by Chad Priest, from Building Vodou in Public.




Top comments (0)