People who run into MCP server problems in Claude Code tend to describe three scenarios: the server never starts, it starts but disconnects after a few minutes, or it's running fine but specific tools don't show up. In most cases, the root cause is something different from what the error message suggests.
This guide walks through a systematic diagnostic process—from configuration validation all the way to multi-server production patterns.
Understanding How MCP Communication Works
Before diving into diagnostics, a quick orientation on how Claude Code and MCP servers actually talk to each other.
MCP (Model Context Protocol) is the protocol Claude uses to communicate with external tools and data sources. There are two transport types:
stdio transport (standard I/O)
The default for locally running servers. Claude Code spawns the server process as a child process and exchanges JSON-RPC messages over stdin/stdout.
Claude Code
↓ spawn
[MCP server process]
↑↓ stdin/stdout (JSON-RPC)
HTTP+SSE transport
For remote servers or long-running processes. You specify a url in the config file and Claude communicates over HTTP.
Knowing this upfront lets you immediately ask: is the failure a process-launch problem or a network-layer problem? That one question narrows the diagnostic space significantly.
Step 1: Validate the Configuration File
Start here before anything else.
# macOS
cat ~/.claude/claude_desktop_config.json
# Windows (PowerShell)
Get-Content "$env:APPDATA\Claude\claude_desktop_config.json"
# Linux
cat ~/.config/claude/claude_desktop_config.jsonCommon mistake 1: JSON syntax errors
Even if the file looks fine visually, a trailing comma or mismatched quote will break the entire config.
# Validate with jq (install with: brew install jq)
cat ~/.claude/claude_desktop_config.json | jq .
# If you see: parse error (Expected separator between values) at line 8
# That line has a syntax problemPaste the contents into jsonlint.com if you don't have jq handy.
Common mistake 2: Unresolvable command
{
"mcpServers": {
"my-server": {
"command": "my-mcp-server"
}
}
}Unless my-mcp-server is installed globally and on your PATH, this will fail silently. Claude Code can't start a process it can't find.
# Check if the command is reachable
which my-mcp-server || echo "not found"The fix is usually one of these two:
// Option 1: Use npx for packages (no global install needed)
{
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/username/"]
}
// Option 2: Use node with an absolute path to the script
{
"command": "node",
"args": ["/absolute/path/to/server/index.js"]
}Common mistake 3: Missing environment variables
MCP server processes don't inherit your shell environment. Any API key the server needs must be explicitly passed in env.
{
"mcpServers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_TOKEN": "your_token_here"
}
}
}
}After any config change, restart Claude Code completely. The config is only read at startup.
Step 2: Start the Server Manually
Bypassing Claude Code and launching the server from your terminal isolates whether the problem is in the server itself or in Claude Code's launcher.
# Launch a stdio server directly
npx -y @modelcontextprotocol/server-filesystem /Users/username/projects
# A healthy server will wait on stdin (this is correct)
# An unhealthy server will print an error and exit immediatelyIf the server exits immediately, watch the error output. Three things cause this most often:
- Node.js version too old — MCP servers generally require LTS 20+. Check with
node --version. - Missing dependencies — run
npm installin the server directory. - Target path doesn't exist — the filesystem server requires the specified path to actually exist.
Step 3: Read the MCP Logs
Claude Code records MCP communication in log files.
# macOS
tail -f ~/Library/Logs/Claude/mcp*.log
# Linux
tail -f ~/.local/share/claude/logs/mcp*.logThe logs use JSON-RPC format. Here's what to look for:
// A healthy initialization exchange
// → Claude sends:
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{}}}
// ← Server responds:
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{"tools":{}},"serverInfo":{"name":"filesystem","version":"0.6.2"}}}
// An error response
{"jsonrpc":"2.0","id":1,"error":{"code":-32603,"message":"Internal error: EACCES: permission denied, open '/restricted/path'"}}If you see no response to the initialize request, the server isn't running. If you see an error field, that message is the real cause.
Step 4: Diagnose Tool Not Found Issues
If the server is running but Claude says it can't find a specific tool, the problem is in tool registration.
You can probe the tools list manually:
# Send JSON-RPC messages directly to a stdio server
printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}\n{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}\n' \
| npx -y @modelcontextprotocol/server-filesystem /Users/username/If you're building a custom server, verify these implementation details:
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
const server = new Server(
{ name: "my-server", version: "1.0.0" },
{ capabilities: { tools: {} } } // ← tools must be declared here
);
// Must implement the tools/list handler
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "search_docs",
description: "Search project documentation",
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "Search query" }
},
required: ["query"]
}
}
]
};
});
// And the tools/call handler
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (name === "search_docs") {
const results = await performSearch(args.query);
return { content: [{ type: "text", text: results }] };
}
throw new Error(`Unknown tool: ${name}`);
});Missing the capabilities: { tools: {} } declaration is the most common reason tools don't appear.
Diagnosing Intermittent Disconnections
A server that starts correctly but disconnects after a few minutes is the trickiest category to debug.
Cause 1: Unhandled exceptions crashing the process
If an exception propagates out of a request handler, Node.js will terminate the process—and the connection drops.
// ❌ No error handling — any exception kills the process
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const result = await fetchFromDatabase(request.params.arguments.id);
return { content: [{ type: "text", text: result }] };
});
// ✅ Catch and return errors as MCP error responses
server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
const result = await fetchFromDatabase(request.params.arguments.id);
return { content: [{ type: "text", text: result }] };
} catch (error) {
return {
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
isError: true
};
}
});Cause 2: Timeout
Some servers with slow operations get disconnected by the client's timeout before they can respond.
{
"mcpServers": {
"slow-server": {
"command": "node",
"args": ["server.js"],
"timeout": 60000
}
}
}Cause 3: Memory exhaustion
Long-running servers that accumulate data without cleanup will eventually crash. Set an explicit heap limit:
NODE_OPTIONS="--max-old-space-size=512" node server.jsMulti-Server Production Patterns
Once you're running three or more MCP servers for a project, reliability and maintainability start to matter.
Pattern 1: Separate servers by responsibility
Keep servers small and focused. A crash in one server then only affects that domain.
{
"mcpServers": {
"filesystem": { ... },
"github": { ... },
"database": { ... },
"search": { ... }
}
}Pattern 2: Add a health-check tool to every server
A simple ping or health_check tool lets Claude verify a server is alive without needing external tooling.
// Lightweight health check
{
name: "health_check",
description: "Returns server status and version",
inputSchema: { type: "object", properties: {} }
}
// Handler
if (name === "health_check") {
return {
content: [{
type: "text",
text: JSON.stringify({
status: "ok",
version: "1.2.0",
uptime: process.uptime()
})
}]
};
}Pattern 3: Write logs to stderr
In stdio servers, console.log is forbidden—it goes to stdout and corrupts the JSON-RPC stream. Use process.stderr for all server-side logging.
// ✅ Safe logging in an MCP stdio server
const log = (level: string, message: string, data?: unknown) => {
process.stderr.write(JSON.stringify({
level,
timestamp: new Date().toISOString(),
message,
...(data ? { data } : {})
}) + "\n");
};
log("info", "Server started");
log("error", "Database query failed", { error: error.message });Claude Code captures stderr output and includes it in the MCP logs, so this is the right channel for debugging information that needs to survive process restarts.
Most MCP connection failures trace back to three root causes: a broken config file, a command that can't be found, or a server that crashes on unhandled exceptions. Check them in that order and you'll solve the vast majority of issues in under 30 minutes.
The investment in good stderr logging and per-handler error handling pays off quickly—once you have visibility into what the server is actually doing, debugging becomes a matter of reading logs rather than guessing.
Error Pattern 1: "MCP server not found" — Server Not Installed or Incorrect Path
This is the most common error you'll encounter. It means the MCP server either isn't installed on your system or the path configured in Claude is incorrect.
How to Diagnose
Start by checking which MCP servers are currently registered:
# List all available MCP servers
claude mcp list
# Example output:
# Available MCP servers:
# - stdio: node /Users/user/.npm/_npx/mcp-server-stdio/bin/server.js
# - jira: python /opt/mcp-servers/jira_server.py
# - github: node /opt/mcp-servers/github/index.jsIf the server you're trying to use doesn't appear in this list, you'll need to install it or fix its configuration.
Common Causes and Solutions
Cause 1: Server isn't installed
# For npm-based MCP servers
npm install -g @modelcontextprotocol/server-stdio
# For Python-based MCP servers
pip install mcp-server-jira
# After installation, verify the Claude config
cat ~/.claude-config.jsonCause 2: Path is incorrect
Check the Claude Desktop configuration file:
// ~/.claude-config.json — WRONG
{
"mcpServers": {
"jira": {
"command": "python",
"args": ["/wrong/path/to/jira_server.py"]
}
}
}
// CORRECT configuration
{
"mcpServers": {
"jira": {
"command": "python",
"args": ["/opt/homebrew/lib/python3.11/site-packages/mcp_server_jira/server.py"]
}
}
}To find the correct path:
# For Python packages
python -c "import mcp_server_jira; print(mcp_server_jira.__file__)"
# For Node.js packages
npm root -g # shows global package directory
which node-mcp-server # shows command locationVerification Steps
# 1. Test if the server can be started directly
python /opt/homebrew/lib/python3.11/site-packages/mcp_server_jira/server.py
# 2. Look for output like this (success indicator)
# {"jsonrpc": "2.0", "id": 1, "method": "initialize", ...}
# 3. Restart Claude Desktop to reload configurationError Pattern 2: "Permission denied" — Insufficient Access Rights
This error commonly appears in headless environments like GitHub Actions or when running in VM mode (Cowork).
Fixing Permission Issues in GitHub Actions
When MCP servers run in GitHub Actions workflows, the current directory often lacks proper permissions:
# .github/workflows/claude-code.yml example
name: Claude Code Automation
on:
push:
branches: [main]
jobs:
auto-generate-content:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Explicitly grant permissions
- name: Fix permissions
run: |
chmod +x scripts/mcp-server.js
chmod -R 755 /tmp/mcp-runtime
- name: Run Claude Code with MCP
env:
CLAUDE_API_KEY: ${{ secrets.CLAUDE_API_KEY }}
run: |
mkdir -p /tmp/mcp-runtime
cp scripts/mcp-server.js /tmp/mcp-runtime/
chmod +x /tmp/mcp-runtime/mcp-server.js
claude code --mcp-server /tmp/mcp-runtime/mcp-server.jsUsing the /permissions Command
You can check what permissions the current process has within Claude Desktop:
# Run directly in Claude Desktop input
/permissions
# Output example:
# Current working directory: /tmp/mcp-runtime
# Process UID: 501
# Process GID: 20
# Directory permissions: drwxr-xr-x
# File permissions: -rwxr-xr-xIf you see missing r (read) or x (execute) permissions, fix them:
# Grant read and execute permissions to directories
chmod -R u+rx /path/to/mcp-server
# Make scripts executable
chmod +x /path/to/mcp-server/bin/start.sh
# Verify
ls -la /path/to/mcp-server/Fixing in Cowork VM Mode
If the filesystem isn't recognized correctly within Cowork:
# 1. Check mount status
mount | grep -E "mcp|workspace"
# 2. Re-select the workspace folder (from Cowork settings)
# 3. Restart the Cowork VM
# 4. Clear cache
rm -rf ~/.cowork/cacheError Pattern 3: OAuth Token Expiration — External Service Authentication
MCP servers that integrate with external services like Jira or GitHub rely on OAuth tokens, which expire after a certain period.
Signs of Token Expiration
[ERROR] OAuth token expired
[ERROR] 401 Unauthorized: Invalid credentials
[ERROR] Authentication failed: Token refresh required
How to Refresh Tokens
# 1. Check current token status
cat ~/.mcp-tokens/jira.json
# 2. Trigger re-authentication
claude mcp auth jira
# 3. Your browser will open asking for authorization — approve it
# 4. Verify the token was updated
cat ~/.mcp-tokens/jira.json
# 5. Restart Claude DesktopFor a deeper dive into OAuth authentication with MCP servers, see the Claude Code MCP OAuth Authentication Guide.
Error Pattern 4: Tool Execution Timeout — Operation Takes Too Long
When an MCP tool takes longer to execute than Claude's timeout setting allows, you'll see a timeout error.
Symptoms
[ERROR] Tool execution timeout after 30s
[ERROR] Transport closed: Server did not respond in time
Adjusting Timeout Values
Edit the Claude Desktop configuration:
// ~/.claude-config.json
{
"mcpServers": {
"jira": {
"command": "python",
"args": ["/opt/homebrew/lib/python3.11/site-packages/mcp_server_jira/server.py"],
"timeout": 120000 // milliseconds (default: 30000 = 30 seconds)
}
},
"toolExecutionTimeout": 120000 // global timeout for all tools
}Finding Performance Bottlenecks with Logs
# Enable debug logging to see execution times
CLAUDE_DEBUG=mcp claude code
# stderr output will include timing information:
# [MCP] Tool invocation: search_issues
# [MCP] Elapsed: 5234ms
# [MCP] Result size: 2.3 MBIf you notice results are very large, consider implementing pagination or filtering to reduce data volume.
Error Pattern 5: "Transport closed" — Server Process Crashes
When an MCP server crashes or runs out of memory, you'll see a transport closed error.
Symptoms
[ERROR] Transport closed
[ERROR] Connection lost to MCP server
[ERROR] Unexpected end of stream
Checking Server stderr for Clues
# 1. Run the MCP server in foreground to see errors directly
python /opt/homebrew/lib/python3.11/site-packages/mcp_server_jira/server.py 2>&1
# 2. Example of a memory exhaustion crash:
# Traceback (most recent call last):
# File "/opt/...", line 45, in handle_request
# result = process_large_dataset()
# MemoryError: Unable to allocate 2.5 GiB for array
# 3. Set memory limits before restarting
python -c "import resource; resource.setrlimit(resource.RLIMIT_AS, (4*1024*1024*1024, -1))" && python server.pyProcess Management for Long-Running Servers
For production use, manage MCP servers with a process supervisor like supervisor:
# Install supervisor
brew install supervisor
# Create configuration file
cat > /opt/homebrew/etc/supervisor.d/mcp-jira.conf << 'EOF'
[program:mcp-jira]
command=python /opt/homebrew/lib/python3.11/site-packages/mcp_server_jira/server.py
autostart=true
autorestart=true
stderr_logfile=/var/log/mcp-jira.err.log
stdout_logfile=/var/log/mcp-jira.out.log
EOF
# Start supervisor
supervisord -c /opt/homebrew/etc/supervisord.confError Pattern 6: Silent Failures — No Error Message Appears
The most frustrating case: MCP tools fail silently without any error output, and you simply get no result.
Debugging with --mcp-debug Flag
# Run with verbose MCP debugging enabled
claude code --mcp-debugExample debug output:
[DEBUG] MCP Request: {
"method": "call_tool",
"params": {
"name": "search_issues",
"arguments": {
"query": "project = MYPROJ",
"limit": 100
}
}
}
[DEBUG] MCP Response (4234ms): {
"result": null,
"error": "JIRA API returned 403: Insufficient permissions"
}
Saving Logs to a File
# Capture all output to a log file
claude code --mcp-debug > /tmp/claude-mcp.log 2>&1
# Monitor logs in real-time
tail -f /tmp/claude-mcp.logVerifying JSON Response Structure
Check that the server is returning valid JSON:
# Start the server and send a test request via stdin
echo '{"jsonrpc":"2.0","method":"call_tool","params":{"name":"search_issues","arguments":{"query":"key=MYPROJ-1"}},"id":1}' | \
python /opt/homebrew/lib/python3.11/site-packages/mcp_server_jira/server.pyHealthy response:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [{"type": "text", "text": "Issue: MYPROJ-1\nTitle: ..."}]
}
}Quick Reference: Connection Troubleshooting Checklist
Before diving into debugging, run through this checklist:
- [ ] Server appears in
claude mcp listoutput - [ ] Configuration file path is correct and file exists
- [ ] File has execute permissions (run
ls -laand look forx) - [ ] OAuth token is current (
claude mcp auth {service}has been run recently) - [ ] Timeout settings are appropriate (use 60+ seconds for slow operations)
- [ ] Debug logs show no errors (
--mcp-debugoutput is clean) - [ ] Memory usage is within limits (check with
top)
A Server Disappears in Another Directory — Scope Mismatch
A server that showed up in claude mcp list vanishes from the list the moment you open a different project. You didn't touch the config file. This is almost never a configuration error — it's the scope the server was registered under.
claude mcp add takes a -s (--scope) flag that decides where the registration lives, with three values:
- local (default): usable only inside the project where you added it, only by you. Omitting
-slands here. - project: written to a
.mcp.jsonat the repo root and shared with your team — anyone who clones the repo gets it. - user: available across all of your own projects.
So if you register with claude mcp add github -- npx -y @modelcontextprotocol/server-github and skip the scope, it becomes specific to that one directory. Open Claude Code in another folder and it's simply not there. That's the real story behind "it worked yesterday."
To isolate it, run claude mcp list in the directory where the problem occurs first. If the server is missing there, suspect a scope mismatch.
# Check which scope a server is registered under
claude mcp get github
# Re-add at user scope if you want it everywhere
claude mcp add -s user github -- npx -y @modelcontextprotocol/server-github
# Use project scope to share with the team (generates .mcp.json in the repo)
claude mcp add -s project github -- npx -y @modelcontextprotocol/server-githubAnyone who receives a .mcp.json via git clone is prompted to approve it on first use. When "the config file exists but the tool won't fire," check that you didn't dismiss that approval prompt.
As an indie developer I move between several repositories in a day, and for a while I kept re-registering the same MCP servers in each one. Once I started using the same tools across the Dolice Labs blogs, I settled on a simple rule: shared tools go in user scope, repo-specific ones in project scope. The "where did it go" panic stopped after that. Scope isn't a failure mode — it's a declaration of where a server is meant to be used, and reading it that way makes it much easier to handle.
Looking back
MCP connection errors fall into six categories: server discovery, permissions, token expiration, timeouts, crashes, and silent failures. When you hit a connection problem, start with claude mcp list to verify the server is recognized, then use --mcp-debug to see what's actually happening under the hood. And when a server you did register won't show up, don't forget to check the scope.
For deeper exploration of MCP implementation patterns, check out Claude Code MCP Server Practical Guide. For authorization and connector design, see Single-Source Authorization for MCP Connectors.