An MCP Server Reporting "Connected" Can Still Be Broken¶
Both Claude clients give you a green light that means far less than it looks like:
- Claude Code —
claude mcp get <name>printsStatus: ✔ Connected - Claude Desktop —
~/Library/Logs/Claude/mcp.loglogs[name] Server started and connected successfully
Neither statement means the server works. Both mean only that the binary spawned and completed the MCP handshake (initialize + tools/list). A server can report Connected while its credential is wrong, its write capability is disabled, or the binary is four versions stale — because none of those are exercised during a handshake.
This bit a fleet-wide mail-mcp rollout on 2026-08-07: one machine had shown a healthy mail server in Claude Desktop since May, and had been silently read-only the entire time.
The Short Answer¶
Drive the server directly over stdio JSON-RPC and call a tool that actually touches the backend. This works mid-session — no Claude restart, no new session required — because you are running the binary yourself, not through the client:
# Export the same env vars the MCP client passes it, then:
{
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"verify","version":"1"}}}'
printf '%s\n' '{"jsonrpc":"2.0","method":"notifications/initialized"}'
printf '%s\n' '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
printf '%s\n' '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"imap_verify_account","arguments":{}}}'
sleep 12
} | /path/to/your-mcp-server 2>/dev/null | python3 -c "
import sys, json
for line in sys.stdin:
try: m = json.loads(line)
except: continue
if m.get('id') in (2, 3):
print(m.get('id'), '->', str(m.get('result') or m.get('error'))[:300])
"
id=2 proves the handshake (what "Connected" already told you). id=3 is the real test — pick whichever tool authenticates or reaches the backend (*_verify_account, list_*, a read that requires credentials). Anything that returns status: ok from the far side is proof; a tools/list result is not.
Why "Connected" Proves So Little¶
An MCP client's job at startup is narrow: spawn the process, exchange initialize, request the tool list. If the server process stays alive and answers those, the client reports success. Servers typically defer their real work — opening an IMAP connection, authenticating, hitting an API — until a tool is actually called, so a bad credential surfaces on first use, long after the client declared victory.
Claude Desktop is even more optimistic: Server started and connected successfully is logged before the child process is confirmed running. The Fantastical MCP permission-denied case shows the consequence — that success line appears immediately above a spawn failure in the same log.
Three Ways a "Connected" Server Is Still Broken¶
1. The credential never authenticates¶
The most common case. Handshake passes, every tool call fails with an auth error. Nothing in the startup logs hints at it.
Check: call a verify/list tool as above. Do not confirm a password by printing it — compare hashes instead, so the secret never lands in a terminal, transcript, or log:
python3 -c "
import json, os, hashlib
h = lambda s: hashlib.sha256(s.encode()).hexdigest()[:12]
cfg = json.load(open(os.path.expanduser('~/Library/Application Support/Claude/claude_desktop_config.json')))
print('config:', h(cfg['mcpServers']['mail']['env']['MAIL_IMAP_DEFAULT_PASS']))
print('source:', h(os.environ['EXPECTED_PASS']))
"
2. A capability flag is off, so it works but does nothing¶
The nastiest failure, because reads succeed and everything looks healthy. mail-mcp gates writes behind MAIL_IMAP_WRITE_ENABLED / MAIL_SMTP_WRITE_ENABLED. With both false, the server connects, lists mailboxes, reads messages — and cannot send a thing. If sending is the reason you deployed it, the deployment achieved nothing while presenting as fine.
Check: read the flags out of the config, don't assume the defaults:
python3 -c "
import json, os
e = json.load(open(os.path.expanduser('~/Library/Application Support/Claude/claude_desktop_config.json')))['mcpServers']['mail']['env']
print({k: v for k, v in e.items() if 'ENABLED' in k or 'SAVE' in k})
"
3. The binary is stale, or was copied in rather than built¶
A binary sitting at the expected path satisfies every existence check while being unmaintainable and out of date. The 2026-08-07 case: ~/projects/mail-mcp contained only target/release/mail-mcp — no .git, no Cargo.toml. The binary had been copied from another machine months earlier, so there was no source to rebuild or update from, and it was four releases behind the rest of the fleet.
Check: confirm the source is actually there, not just the artifact:
ls -la ~/projects/<server>/.git ~/projects/<server>/Cargo.toml # or package.json, pyproject.toml
ls -l ~/projects/<server>/target/release/<server> # how old is the binary?
A Fourth Trap: The Account ID¶
Servers supporting multiple accounts namespace their env vars (mail-mcp: MAIL_IMAP_<ID>_HOST). A config using MAIL_IMAP_MAJORMAIL_* while every tool call addresses the implicit DEFAULT account will connect fine and then behave as if nothing is configured.
This also breaks naive verification: grepping for MAIL_IMAP_DEFAULT_PASS on such a config returns nothing and reads as "no password configured" — when the password is present under a different ID. Enumerate the keys before concluding anything is missing:
python3 -c "
import json, os
e = json.load(open(os.path.expanduser('~/Library/Application Support/Claude/claude_desktop_config.json')))['mcpServers']['mail']['env']
print(sorted(k for k in e if 'PASS' not in k)) # names only, no secrets
"
Checklist Before Calling a Machine "Done"¶
tools/callagainst a real backend tool returnsstatus: ok— not merelytools/list- Credential hash-matches its source of truth (never printed)
- Capability flags (
*_WRITE_ENABLED, etc.) are set to what you actually intend - Source tree present, so the thing can be rebuilt — not an orphan binary
- Account/namespace ID matches what your tool calls will address
- Version matches the rest of the fleet
Restarting a Client to Pick Up Config Changes¶
Editing claude_desktop_config.json does nothing until Desktop restarts — the old child process keeps running from the old environment. Quit gracefully rather than force-killing, and check nothing is mid-task first:
pgrep -lf "cowork|vzgvisor" # don't interrupt a running VM/task
osascript -e 'quit app "Claude"'
sleep 6
open -a Claude
grep -i "\[mail\]" ~/Library/Logs/Claude/mcp.log | tail -8
Expect: clean shutdown lines, then Initializing server... → Server started and connected successfully → initialize result → tools/list result. One caution when reading that log — errors from the previous instance's shutdown sit in the same file. Check line position and timestamps before attributing an error to the new process.
Related¶
- Fantastical MCP Server: Permission Denied on Launch — the spawn-time failure mode, where the same optimistic success line precedes a real failure
- Keep Secrets Out of claude_desktop_config.json — wrapper +
0600env file, avoiding plaintext credentials in the config this article keeps reading - MCP Python SDK 2.0 Breaks Unpinned uv-run Servers — another "was fine yesterday" MCP breakage
- Claude Desktop MCP Mass-Disconnect After Blocking SSH Reboot