DEV Community

Thales Augusto
Thales Augusto

Posted on

How to Use Paper MCP Server Inside a Dev Container

If you use Paper as your MCP server and develop inside a Docker-based dev container (VS Code Dev Containers, Cursor, or any devcontainer-compatible editor), you've probably hit this frustrating wall:

Failed to reconnect to plugin:paper-desktop:paper: ECONNRESET

Your MCP client inside the container tries to reach 127.0.0.1:29979, but that address points to the container's own loopback — not your host machine where Paper is actually running. This article walks through the exact solution.


Why This Happens

When you run a dev container, your development environment lives inside a Docker container. Your .mcp.json file correctly points to http://127.0.0.1:29979/mcp — and that works fine when running tools directly on the host. But inside the container:

  • 127.0.0.1 refers to the container's loopback, not the host's
  • Paper MCP server is bound to 127.0.0.1 on the host (localhost only)
  • Docker containers communicate with the host via the bridge network gateway (e.g., 172.20.0.1) — and Paper rejects connections from that IP

Why ports: "29979:29979" doesn't solve it

A common first instinct is to add this to docker-compose.yaml:

ports:
  - "29979:29979"
Enter fullscreen mode Exit fullscreen mode

This does the opposite of what you need. It exposes a container port to the host, not the other way around. The traffic direction you need is: container → host.


The Solution: A Two-Hop socat Relay

The fix requires two socat relays working in tandem:

MCP Client (container)
  → socat (container: 0.0.0.0:29979)
    → socat (host: 172.20.0.1:29979)
      → Paper MCP (host: 127.0.0.1:29979)
Enter fullscreen mode Exit fullscreen mode

Why two hops? The host relay is necessary because Paper only accepts connections from 127.0.0.1. When container socat connects to the host via the bridge gateway IP, the host sees it as an external connection and Paper rejects it. The host-side socat acts as a local proxy that makes the connection appear to come from localhost.


Step 1 — Install socat in the Dev Container

Add socat to your .devcontainer/Dockerfile:

# Install socat for MCP host port forwarding
USER root
RUN apt-get update && apt-get install -y --no-install-recommends socat && rm -rf /var/lib/apt/lists/*
Enter fullscreen mode Exit fullscreen mode

Step 2 — Start the In-Container Relay via Docker Compose

In .devcontainer/compose.yaml, replace the default command: sleep infinity with a command that starts socat before keeping the container alive. The $$ syntax escapes $ for Docker Compose so the shell receives it correctly:

services:
  rails-app:  # or whatever your service is named
    command: >
      /bin/sh -c "socat TCP-LISTEN:29979,fork,reuseaddr
      TCP:$$(ip route show default | awk '{print $$3}'):29979
      & sleep infinity"
Enter fullscreen mode Exit fullscreen mode

This socat process listens on port 29979 inside the container and forwards traffic to the Docker gateway IP (the host), where the second relay will be waiting.


Step 3 — Set Up the Host-Side Relay

On your host machine, create a relay script that dynamically resolves the Docker bridge gateway and starts socat bound to it:

# ~/.local/bin/paper-mcp-bridge
#!/bin/bash
NETWORK_NAME="your_project_default"  # adjust to your compose project name

echo "[paper-mcp-bridge] Waiting for Docker network '$NETWORK_NAME'..."
while true; do
  DOCKER_GW=$(docker network inspect "$NETWORK_NAME" \
    --format '{{range .IPAM.Config}}{{.Gateway}}{{end}}' 2>/dev/null)
  if [ -n "$DOCKER_GW" ]; then
    break
  fi
  sleep 3
done

echo "[paper-mcp-bridge] Gateway: $DOCKER_GW — starting relay..."
exec socat TCP-LISTEN:29979,fork,reuseaddr,bind="$DOCKER_GW" TCP:127.0.0.1:29979
Enter fullscreen mode Exit fullscreen mode

Finding your network name: Run docker network ls and look for the network associated with your compose project. It's typically <project_name>_default.

Make it executable:

chmod +x ~/.local/bin/paper-mcp-bridge
Enter fullscreen mode Exit fullscreen mode

Step 4 — Persist with a systemd User Service

Create ~/.config/systemd/user/mcp-bridge.service:

[Unit]
Description=MCP Bridge — Relay Docker container access to Paper MCP server (port 29979)
After=default.target

[Service]
Type=simple
ExecStartPre=-/usr/bin/pkill -f "socat TCP-LISTEN:29979"
ExecStart=%h/.local/bin/paper-mcp-bridge
Restart=on-failure
RestartSec=5

[Install]
WantedBy=default.target
Enter fullscreen mode Exit fullscreen mode

Enable and start it:

systemctl --user daemon-reload
systemctl --user enable mcp-bridge.service
systemctl --user start mcp-bridge.service
Enter fullscreen mode Exit fullscreen mode

The service will:

  • Start automatically when you log in
  • Wait for the Docker network to be available before binding
  • Restart automatically if it crashes
  • Kill any leftover socat processes before starting (ExecStartPre=-)

Step 5 — Keep Your .mcp.json Unchanged

No changes needed to your MCP config:

{
  "mcpServers": {
    "paper": {
      "url": "http://127.0.0.1:29979/mcp"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

127.0.0.1:29979 works on the host directly, and inside the container the in-container socat intercepts it transparently.


Testing It

After rebuilding the container, verify from inside it:

# Check socat is listening
ss -tlnp | grep 29979

# Test the full chain reaches Paper MCP
bash -c "echo > /dev/tcp/127.0.0.1/29979 && echo OK || echo FAILED"
Enter fullscreen mode Exit fullscreen mode

A successful OK means the two-hop relay is working end-to-end.

On the host, verify Paper MCP is responding:

curl -v http://127.0.0.1:29979/mcp
# Expect: HTTP 404 {"error":"not_found","error_description":"Session not found"}
# (This is normal — it means the server is up and responding)
Enter fullscreen mode Exit fullscreen mode

Summary

Component What it does
socat in Dockerfile Installs the relay tool in the container image
compose.yaml command Starts container-side socat on port 29979 → host gateway
paper-mcp-bridge script Host-side socat bound to Docker bridge IP → localhost:29979
mcp-bridge.service Persists the host relay across reboots via systemd

The key insight is that Paper MCP only accepts localhost connections, so you need a relay on the host itself — not just port forwarding. The container relay handles the 127.0.0.1 address from MCP clients; the host relay makes those connections appear local to Paper.

Top comments (0)