Posted on
Artificial Intelligence

MCP Debugging Guide

Author
  • User
    linuxbash
    Posts by this author
    Posts by this author

MCP Debugging Guide: How to See, Trace, and Fix What Your Client Can’t

Ever had an MCP client hang on “initializing…”, fail to discover tools, or mysteriously time out? When your model-to-tool bridge goes opaque, Bash is your best flashlight. This guide shows you how to make MCP traffic visible, confirm environment correctness, and pinpoint failures using nothing more than standard Linux tooling.

What you’ll get:

  • A practical toolkit you can install with apt, dnf, or zypper

  • Proven steps to surface the MCP protocol, validate JSON-RPC interactions, and trace misbehaving processes

  • Real-world patterns to fix PATH, permissions, and handshake issues

Note: MCP implementations typically speak JSON-RPC 2.0 via stdio or a socket. Exact framing (e.g., newline-delimited JSON vs. Content-Length headers) depends on the server. Always check your server’s documentation for specifics; the techniques below are protocol-agnostic.

Why this matters

  • Text protocols are debuggable with shell tools. MCP messages are just JSON; pretty-printing, grepping, and diffing go a long way.

  • Environment differences cause most “works-on-my-machine” bugs. Systemd services, containers, and shells don’t share PATHs, ulimits, or permissions.

  • Syscall tracing answers “why” with evidence. If a server claims a tool is missing, strace will show ENOENT in black and white.

If you can read JSON, watch file descriptors, and check environment variables, you can debug MCP anywhere Linux runs.

0) Install your debugging toolkit

Install these once; they’ll pay for themselves on your next incident:

  • jq: JSON pretty-printer and validator

  • socat or nc: socket piping, quick probes

  • strace and lsof: syscall and file/socket inspection

  • ripgrep (rg): fast searches through logs/configs

  • curl: HTTP checks when MCP tools call web services

Debian/Ubuntu (apt):

sudo apt update
sudo apt install -y jq socat strace lsof netcat-openbsd ripgrep curl

Fedora/RHEL/CentOS (dnf):

sudo dnf install -y jq socat strace lsof nmap-ncat ripgrep curl

openSUSE/SLE (zypper):

sudo zypper refresh
sudo zypper install -y jq socat strace lsof netcat-openbsd ripgrep curl

1) Surface the wire: make MCP messages visible

Goal: See exactly what the client and server are sending so you can spot malformed JSON, wrong method names, or missing headers.

  • If your MCP server supports TCP, put a transparent logger in the middle with socat:
# Log proxy listening on 4001 and forwarding to the real server on 4000
socat -v -x TCP-LISTEN:4001,fork,reuseaddr TCP:127.0.0.1:4000 2>mcp-wire.log

Then point your client at 127.0.0.1:4001 and tail the log:

tail -f mcp-wire.log | sed -n 's/.*{/{/p' | jq -C . 2>/dev/null
  • If your server is stdio-based, run it standalone and probe with a minimal JSON-RPC request. Adjust framing to match your server (newline-delimited vs. Content-Length):
# Newline-delimited JSON example
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \
  | ./your-mcp-server --stdio 2>server.log \
  | jq -C .

# If your server expects LSP-style framing, include headers:
{
  len=$(printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | wc -c)
  printf 'Content-Length: %d\r\n\r\n' "$len"
  printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'
} | ./your-mcp-server --stdio 2>server.log | hexdump -C
  • Sanity-check JSON quickly:
jq empty <<< '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' && echo OK

Tip: Many servers have a verbose flag or env var (e.g., --verbose, --debug, LOG_LEVEL=debug). Turn it on and capture stderr:

./your-mcp-server --stdio --verbose 2>mcp-debug.log

2) Verify the environment: PATH, permissions, and configuration

Most MCP “tool not found” or “resource unavailable” issues are just environment mismatches between your shell and the runtime (systemd, Docker, or another user).

  • Compare env and PATH:
printf 'Working dir: %s\n' "$PWD"
printf 'User: %s (uid=%s)\n' "$(id -un)" "$(id -u)"
printf 'PATH:\n%s\n' "$PATH"
env | sort > /tmp/shell.env

# If running under systemd:
systemctl show your-mcp.service | grep -E 'Environment=|User='
  • Make sure the server can see the same executables:
type -a git
which python3
ls -l "$(which git)"
  • Check permissions on configs, sockets, and cache dirs:
namei -l /var/lib/your-mcp
ls -l /run/your-mcp.sock
  • SELinux/AppArmor denials can masquerade as generic failures:
# SELinux
getenforce 2>/dev/null || true
sudo ausearch -m avc -ts recent 2>/dev/null | tail

# AppArmor (Debian/Ubuntu)
dmesg | grep -i apparmor | tail
  • Systemd service overrides to fix PATH or env:
sudo systemctl edit your-mcp.service
# Add lines like:
# [Service]
# Environment="PATH=/usr/local/bin:/usr/bin:/bin"
# Environment="LOG_LEVEL=debug"

sudo systemctl daemon-reload
sudo systemctl restart your-mcp.service
journalctl -u your-mcp.service -f

3) Trace the process: find ENOENTs, EACCESes, and timeouts

When logs are vague, syscalls are truth.

  • Follow syscalls with timestamps; look for failing open(), connect(), read()/write() stalls:
sudo strace -ff -tt -s 200 -o /tmp/mcp.strace.log ./your-mcp-server --stdio
# Or attach to a running pid:
pidof your-mcp-server
sudo strace -p <PID> -tt -s 200 -o /tmp/mcp.attach.log
  • Typical smoking guns:

    • ENOENT on a tool: the PATH is wrong or the binary is missing
    • EACCES on a config file: fix permissions or ownership
    • ECONNREFUSED/ETIMEDOUT: wrong host/port or firewall
    • read(0, …) blocking forever: framing mismatch; client and server disagree on how to delimit messages
  • Inspect open files and sockets:

sudo lsof -p <PID> -nP
  • Bound but not listening? Check port and address family:
sudo lsof -i -nP | rg your-mcp
ss -ltnp | rg your-mcp

4) Validate JSON-RPC behavior, not just syntax

A well-formed message can still be the wrong message.

  • Confirm request/response pairing and id types:
rg -N '"id":' mcp-wire.log
  • Check method names and params agreed upon by client/server:
rg -N '"method":' mcp-wire.log | sort | uniq -c
  • Triage errors quickly:
rg -N '"error":' mcp-wire.log
  • Build minimal repros you can paste into a socket:
# If your server is on TCP 4000 and expects newline-delimited JSON
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \
  | nc 127.0.0.1 4000 | jq -C .
  • Compact JSON for clients that need single-line frames:
jq -c '{jsonrpc:"2.0",id:1,method:"initialize",params:{}}'

5) Real-world fixes

  • Tool discovery fails under systemd but works in your shell

    • Symptom: Server logs “git not found”; strace shows ENOENT on /usr/bin/git
    • Fix: Set PATH in the systemd unit, or use absolute paths in the server config
    • Verify: systemctl show your-mcp.service | rg Environment=
  • Handshake hangs after connect

    • Symptom: Client times out; strace shows server read(0,…) blocking
    • Cause: Framing mismatch (e.g., client expects Content-Length headers; server sends bare lines)
    • Fix: Align framing per server docs; test with a minimal request via nc/socat
  • Permission denied opening state dir

    • Symptom: EACCES on /var/lib/your-mcp/state.json
    • Fix: sudo chown -R yourmcpuser:yourmcpuser /var/lib/your-mcp && chmod -R u+rwX /var/lib/your-mcp
  • Conflicting socket

    • Symptom: Address already in use (EADDRINUSE) on startup
    • Fix: Change port, free the socket, or stop the conflicting service
    • Verify: ss -ltnp | rg :4000

A quick MCP debug checklist

  • Can I see the messages? If not, add a TCP proxy log (socat -v) or enable verbose server logs.

  • Is the environment what I think it is? Compare PATH, user, working dir between shell and runtime.

  • What does strace say? Search for ENOENT/EACCES/ECONNREFUSED/ETIMEDOUT.

  • Are JSON-RPC ids paired and methods spelled correctly? Grep the wire logs.

  • Can I reduce to a minimal repro? Send a one-line initialize via nc and observe the response.

Conclusion and next steps

You don’t need a bespoke IDE to debug MCP—just good shell habits. Make the wire visible, confirm the environment, and let syscalls tell you the truth. From there, most problems are straightforward to fix.

Your next step:

  • Install the toolkit with your package manager, enable verbose logging on your MCP server, and capture a short failing run.

  • If you’re stuck, share a redacted snippet of the wire log (requests/responses and errors) and the relevant strace lines. With that, you or your team can usually diagnose the issue in minutes.

If you want a deeper dive, create a reproducible “init, list-tools, run-tool” script with nc/jq tailored to your server’s framing. That little harness becomes your smoke test for every deployment.