Skip to content

Instantly share code, notes, and snippets.

@tamirdresher
Created December 15, 2025 06:53
Show Gist options
  • Select an option

  • Save tamirdresher/53be74dcefe86272bb7af442333b36f4 to your computer and use it in GitHub Desktop.

Select an option

Save tamirdresher/53be74dcefe86272bb7af442333b36f4 to your computer and use it in GitHub Desktop.
MCP stdio Proxy for Linux/macOS - Debug Model Context Protocol servers by intercepting and logging all JSON-RPC communication. Cross-platform support for Linux, macOS, and Unix systems
#!/usr/bin/env bash
# MCP stdio Proxy - Bash Version
#
# A transparent proxy for debugging Model Context Protocol (MCP) servers.
# Intercepts and logs all JSON-RPC communication between MCP client and server.
#
# Usage: bash mcp-stdio-proxy.sh <command> [log_file] [args...]
# or: ./mcp-stdio-proxy.sh <command> [log_file] [args...]
#
# Example: ./mcp-stdio-proxy.sh npx logs/mcp.log @playwright/mcp@latest
#
# Features:
# - Full JSON-RPC message logging (client ↔ server)
# - Process tree analysis
# - Environment variable dumps
# - Transparent stdio forwarding
# - Exit code preservation
# - Cross-platform (Linux, macOS, Unix, WSL)
#
# Blog post: https://your-blog.com/debugging-mcp-servers
# Repository: https://github.com/your-repo
set -euo pipefail
# Parse arguments
if [ $# -lt 1 ]; then
echo "Usage: $0 <command> [log_file] [args...]" >&2
exit 1
fi
COMMAND="$1"
shift
# Determine log file path
if [ $# -ge 1 ] && [[ ! "$1" =~ ^- ]]; then
LOG_FILE="$1"
shift
else
# Default to scripts folder
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOG_FILE="$SCRIPT_DIR/mcp_communication.log"
fi
# Convert to absolute path if relative
if [[ ! "$LOG_FILE" = /* ]]; then
LOG_FILE="$(pwd)/$LOG_FILE"
fi
# Ensure log directory exists
LOG_DIR="$(dirname "$LOG_FILE")"
mkdir -p "$LOG_DIR"
# Get timestamp function
get_timestamp() {
date '+%Y-%m-%d %H:%M:%S'
}
get_timestamp_ms() {
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS
python3 -c "import datetime; print(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3])"
else
# Linux
date '+%Y-%m-%d %H:%M:%S.%3N'
fi
}
# Log startup
TIMESTAMP=$(get_timestamp)
WORKING_DIR="$(pwd)"
PROXY_PID=$$
{
echo "[$TIMESTAMP] Starting MCP stdio proxy"
echo "[$TIMESTAMP] Working Directory: $WORKING_DIR"
echo "[$TIMESTAMP] Proxy Script PID: $PROXY_PID"
echo "[$TIMESTAMP] Command to execute: $COMMAND $*"
echo "[$TIMESTAMP]"
} >> "$LOG_FILE"
# Walk up the process tree
{
echo "[$TIMESTAMP] ========== PROCESS TREE =========="
CURRENT_PID=$PROXY_PID
DEPTH=0
MAX_DEPTH=10
while [ $DEPTH -lt $MAX_DEPTH ] && [ $CURRENT_PID -gt 1 ]; do
INDENT=$(printf '%*s' $((DEPTH * 2)) '')
# Get process info (different commands for Linux vs macOS)
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS
PROC_INFO=$(ps -p $CURRENT_PID -o comm=,args= 2>/dev/null || echo "unknown")
PROC_NAME=$(echo "$PROC_INFO" | awk '{print $1}' | xargs basename)
PROC_CMD=$(echo "$PROC_INFO" | cut -d' ' -f2-)
else
# Linux
PROC_NAME=$(ps -p $CURRENT_PID -o comm= 2>/dev/null || echo "unknown")
PROC_CMD=$(ps -p $CURRENT_PID -o args= 2>/dev/null || echo "unknown")
fi
echo "[$TIMESTAMP] $INDENT[$DEPTH] $PROC_NAME (PID: $CURRENT_PID)"
# Truncate long command lines
if [ ${#PROC_CMD} -gt 300 ]; then
PROC_CMD="${PROC_CMD:0:300}... (truncated)"
fi
echo "[$TIMESTAMP] $INDENT CommandLine: $PROC_CMD"
# Get parent PID
if [[ "$OSTYPE" == "darwin"* ]]; then
PARENT_PID=$(ps -p $CURRENT_PID -o ppid= 2>/dev/null | tr -d ' ' || echo "0")
else
PARENT_PID=$(ps -p $CURRENT_PID -o ppid= 2>/dev/null | tr -d ' ' || echo "0")
fi
if [ -z "$PARENT_PID" ] || [ "$PARENT_PID" -le 1 ]; then
break
fi
CURRENT_PID=$PARENT_PID
DEPTH=$((DEPTH + 1))
done
echo "[$TIMESTAMP] ========================================"
echo ""
} >> "$LOG_FILE"
# Resolve command path
COMMAND_PATH=$(command -v "$COMMAND" 2>/dev/null || echo "$COMMAND")
TIMESTAMP=$(get_timestamp)
echo "[$TIMESTAMP] Resolved command path: $COMMAND_PATH" >> "$LOG_FILE"
# Log environment variables
{
TIMESTAMP=$(get_timestamp)
echo "[$TIMESTAMP] ========== ENVIRONMENT VARIABLES =========="
# Sort and display environment variables
while IFS='=' read -r name value; do
# Truncate very long values (like PATH)
if [ ${#value} -gt 200 ]; then
DISPLAY_VALUE="${value:0:200}... (truncated, length: ${#value})"
else
DISPLAY_VALUE="$value"
fi
echo "[$TIMESTAMP] ENV: $name = $DISPLAY_VALUE"
done < <(env | sort)
echo "[$TIMESTAMP] ========================================"
echo ""
} >> "$LOG_FILE"
# Create named pipes for logging
TEMP_DIR=$(mktemp -d)
STDIN_PIPE="$TEMP_DIR/stdin.pipe"
STDOUT_PIPE="$TEMP_DIR/stdout.pipe"
STDERR_PIPE="$TEMP_DIR/stderr.pipe"
mkfifo "$STDIN_PIPE" "$STDOUT_PIPE" "$STDERR_PIPE"
# Cleanup function
cleanup() {
rm -rf "$TEMP_DIR"
# Kill background jobs
jobs -p | xargs -r kill 2>/dev/null || true
}
trap cleanup EXIT INT TERM
# Start the child process with redirected streams
TIMESTAMP=$(get_timestamp)
echo "[$TIMESTAMP] Starting child process: $COMMAND_PATH $*" >> "$LOG_FILE"
# Launch child process with redirected stdio
"$COMMAND_PATH" "$@" \
< "$STDIN_PIPE" \
> "$STDOUT_PIPE" \
2> "$STDERR_PIPE" &
CHILD_PID=$!
TIMESTAMP=$(get_timestamp)
echo "[$TIMESTAMP] Process started successfully (PID: $CHILD_PID)" >> "$LOG_FILE"
# Background job to forward stdin and log it
{
while IFS= read -r line; do
TIMESTAMP=$(get_timestamp_ms)
echo "[$TIMESTAMP] CLIENT -> SERVER: $line" >> "$LOG_FILE"
echo "$line"
done < /dev/stdin > "$STDIN_PIPE"
} &
STDIN_PID=$!
# Background job to forward stdout and log it
{
while IFS= read -r line; do
TIMESTAMP=$(get_timestamp_ms)
echo "[$TIMESTAMP] SERVER -> CLIENT: $line" >> "$LOG_FILE"
echo "$line"
done < "$STDOUT_PIPE"
} &
STDOUT_PID=$!
# Background job to forward stderr and log it
{
while IFS= read -r line; do
TIMESTAMP=$(get_timestamp_ms)
echo "[$TIMESTAMP] SERVER STDERR: $line" >> "$LOG_FILE"
echo "$line" >&2
done < "$STDERR_PIPE"
} &
STDERR_PID=$!
# Wait for child process to exit
wait $CHILD_PID
EXIT_CODE=$?
# Give background jobs a moment to finish
sleep 0.5
# Kill forwarding jobs
kill $STDIN_PID $STDOUT_PID $STDERR_PID 2>/dev/null || true
# Log exit
EXIT_TIME=$(get_timestamp)
{
echo ""
echo "[$EXIT_TIME] Process exited with code: $EXIT_CODE"
echo "[$EXIT_TIME] ========================================"
echo ""
} >> "$LOG_FILE"
exit $EXIT_CODE
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment