MCP Connector Troubleshooting: Fix Failed Connections

By Yanni Papoutsis | 11 min read | 2026-07-22

TL;DR: When an MCP connector fails, the fault is almost always in one of five places: the process never started, the transport is mismatched, authentication expired, the server crashed during initialisation, or the tools were registered but never surfaced. Work through them in that order rather than guessing. Start by confirming the server binary or command runs standalone in your terminal, because a server that will not start by hand will never start under a client. Then check the transport: stdio servers must never write anything to stdout except protocol messages, and HTTP servers must be reachable at the URL your client holds. Auth failures show up as a successful connection with zero tools, or a 401 buried in the logs. Read the client logs before touching config, since they tell you which of the five failure classes you are in within about thirty seconds. Most "MCP is broken" reports resolve to a stray print() statement, an expired OAuth token, or a config file with a relative path in it.

Why do MCP connectors fail so often?

Model Context Protocol is a client and server protocol. The client (Claude Desktop, Claude Code, or another MCP host) launches or connects to a server, performs an initialisation handshake, asks the server what tools, resources and prompts it offers, and then calls them on your behalf. Every one of those stages can fail independently, and the failure surfaces to you as the same generic message: the connector is unavailable.

The protocol itself is stable. What breaks is the plumbing around it. Servers run as separate processes with their own environment, their own working directory, and their own idea of where Python or Node lives. The client does not inherit your shell profile. That single fact explains a large share of the failures you will see.

If you built the server yourself, the debugging path is shorter because you control both sides. The patterns in build-mcp-server-python-fastmcp and build-mcp-server-node-sdk assume a working baseline; this guide assumes yours has stopped working.

How do I read MCP logs before changing anything?

Do not edit config until you have read a log. Changing config blind turns one variable into three.

MCP hosts write connector logs to disk. In Claude Desktop the logs live under the application support directory for the app, in a logs subfolder, with one file per server plus a general one. In Claude Code, connector output is surfaced through the CLI's own diagnostics. Rather than memorising a path that may shift between versions, find it once and note it in your own runbook.

  1. Locate the log directory for your MCP host.
  2. Tail the file for the server that is failing, plus the general MCP log.
  3. Restart the client and watch what appears as it starts.
  4. Classify the failure using the table below before you touch anything.
# macOS, Claude Desktop: find the log directory, then tail it
find "$HOME/Library/Logs" -iname "*mcp*" -maxdepth 3 2>/dev/null

# Tail everything MCP-related while you restart the client
tail -f ~/Library/Logs/Claude/mcp*.log

What you see maps to a failure class:

Why does my server work in the terminal but not in the client?

This is the single most common report, and it has three usual causes.

Environment. The client launches your server without your shell profile. nvm, pyenv, asdf, Homebrew paths and anything else you set up in .zshrc do not exist for that process. If your config says python, the client resolves whatever python means in its own minimal environment, which may be nothing at all.

Fix it by using absolute paths to interpreters and scripts:

{
  "mcpServers": {
    "my-server": {
      "command": "/Users/you/.local/share/uv/tools/my-server/bin/python",
      "args": ["/Users/you/projects/my-server/server.py"],
      "env": {
        "MY_API_KEY": "REPLACE_ME_FROM_KEYCHAIN"
      }
    }
  }
}

Verify the interpreter path with which python3 and the script path with realpath server.py, then paste the literal output. Relative paths and ~ expansion are not reliable in this context.

Working directory. Your server may open a file relative to the current directory. Under the client, the current directory is not your project folder. Resolve paths from the module's own location rather than from the process working directory.

stdout pollution. For stdio transport, stdout is the protocol channel. Every byte your server prints to stdout that is not a well-formed JSON-RPC message corrupts the stream and the client drops the connection. A single print("starting up") is enough. Send all diagnostics to stderr instead.

import sys

# Wrong: this corrupts the stdio transport
# print("Server starting")

# Right: stderr is free for logging
print("Server starting", file=sys.stderr)

In Node, console.log writes to stdout and console.error writes to stderr. Use console.error for everything, or configure your logger to target stderr explicitly.

How do I diagnose transport problems?

MCP servers speak over one of two transports. Stdio servers are launched as a child process by the client and communicate over standard input and output. HTTP servers listen on a URL and the client connects to them over the network. The config shape differs, and using the wrong shape produces a connector that never initialises.

For stdio, the client needs command and args. For a remote HTTP server, the client needs a URL. If you have a url key pointing at something that expects to be spawned, or a command for something that is already running as a web service, nothing will work no matter how many times you restart.

Test each independently of the client:

# stdio: does the server start and stay up without exploding?
/absolute/path/to/python /absolute/path/to/server.py
# It should sit there waiting for input. Ctrl-C to exit.
# If it prints a banner or exits immediately, that is your bug.

# HTTP: is the endpoint actually reachable?
curl -i https://your-server.example.com/mcp
# You want a response, not a connection refused or a DNS failure.

If the stdio server exits instantly, run it with your own JSON-RPC initialise message piped in, or simply read the traceback it emits. If it prints anything human-readable before waiting, that output is going to break the transport.

What causes "connected but no tools"?

The handshake succeeded, so the process is alive and the transport is clean. The problem is now in registration or auth.

  1. Confirm the tools are registered. Decorators and registration calls only run if the module containing them is imported. If your tools live in a file that is never imported by the entry point, the server starts perfectly and offers nothing.
  2. Check the tool schemas. A tool whose input schema fails validation may be silently skipped rather than crashing the server. Keep schemas simple and typed.
  3. Check authentication. If the server needs a token to enumerate its own capabilities, and the token is missing or expired, you get a clean connection with an empty tool list. Look for a 401 or 403 in the server's stderr.
  4. Restart the client fully. MCP clients read connector config at startup. Reloading a window is not the same as restarting the application.

Token expiry is the quiet killer here. A connector that worked for weeks and then went empty overnight has almost certainly hit an expired credential rather than a code change. The OAuth patterns behind that are worth understanding properly, and connector auth is covered in depth alongside the general OAuth flow in the wider connector set.

How should I handle secrets in MCP configs?

MCP config files are plain JSON on disk with no encryption. Treat them like source code that must never be committed.

The security posture for skills applies equally to connectors, and the reasoning in skill-security-best-practices transfers directly: least privilege, no secrets in artefacts, rotate on any doubt.

What is the fastest triage order?

Work top to bottom and stop at the first thing that is wrong.

  1. Does the server run standalone from your terminal with absolute paths? If no, fix that first.
  2. Does the client log show a spawn error? If yes, your command path is wrong.
  3. Does the log show your own stack trace? If yes, fix the crash and retry.
  4. Does the server print anything to stdout on startup? If yes, move it to stderr.
  5. Is the transport in your config the right shape for the server? If no, correct it.
  6. Does the connector show zero tools? If yes, check registration, then check auth expiry.
  7. Have you fully quit and relaunched the client? If no, do it before concluding anything.

This order works because each step is cheaper than the one after it and eliminates a whole class of causes. Reversing the order means you will spend an hour on token scopes for a server that was never starting.

How do I stop the same failures recurring?

Build a health check into your workflow. A tiny tool on each server that returns its own version, its config source and whether its credentials are present turns "is it broken" into a single call. Combine that with a scheduled check, along the lines of schedule-recurring-claude-tasks, and you find out about an expired token before it interrupts your work rather than during it.

Keep your connector configs in a version-controlled file with placeholders, and hydrate real values at install time. That way, a corrupted or lost config is a two minute restore rather than an afternoon of remembering what was in it. If you run a filesystem-level connector, the setup notes in set-up-desktop-commander show the same pattern applied to a real server.

Finally, when a connector misbehaves in a way that looks like the model ignoring it, the problem may not be the connector at all. The diagnostic approach in debugging-skill-triggers applies: confirm the capability exists and is discoverable before assuming the plumbing is broken.

Frequently Asked Questions

Why does my MCP server work with the inspector but not with Claude?

The inspector typically launches your server from your shell, inheriting your full environment, your PATH and your working directory. Claude launches it from a minimal environment with none of those. If it works in one and not the other, you have an environment problem: replace every command and path in your config with an absolute one, and pass any required variables explicitly through the config's env block rather than relying on your shell profile.

What does spawn ENOENT actually mean?

It means the operating system could not find the executable named in your config. It is not an MCP error at all, it is the process launcher failing before your code runs. Run which node or which python3 in your terminal, take the full output path, and put that literal string in the command field. If you use a version manager, the shim in your PATH is not visible to the client, so point at the real binary.

Can a single print statement really break the whole connector?

Yes, for stdio transport. Standard output is the wire the protocol runs over, and anything that is not a valid protocol message desynchronises the parser. The client sees malformed data and closes the connection. This is why servers should log to stderr exclusively. If you inherited a codebase with print statements scattered through it, redirect stdout at startup or replace the print calls before you debug anything else.

My connector had tools yesterday and none today. What changed?

Almost always a credential. OAuth access tokens expire on a schedule, refresh tokens can be revoked, and personal access tokens have expiry dates you set months ago and forgot. Check the server's stderr for a 401 or 403. If the server enumerates its tools by calling the upstream API, an expired token produces exactly this symptom: a healthy connection with an empty capability list. Re-authenticate and the tools return.

Should I put API keys directly in the MCP config file?

No. The file is unencrypted JSON that is easy to back up, sync or commit by accident. Reference secrets from your OS keychain or a secrets manager, or use a small wrapper script that exports them before launching the server. Keep a committed template with placeholders like YOUR_GITHUB_TOKEN and sk-REPLACE_ME so the shape is documented without the values. If a real key ever lands in a file you shared, rotate it rather than deleting the file, because the deletion does not un-share it.