Guide · Development Toolchain 2026

MCP Server Dev Container — VS Code devcontainer, consistent environments, Docker

A Dev Container (.devcontainer/devcontainer.json) gives every contributor to an MCP server project the same Node.js version, the same global tools (pnpm, tsx, MCP Inspector CLI), and the same VS Code extensions — without a setup document that goes stale. For MCP servers that interact with external services (databases, APIs, queues), the docker-compose.yml-based Dev Container spins up those services alongside the development environment so contributors can run the full stack locally with docker compose up. The three Dev Container behaviours that require MCP-specific attention: MCP Inspector's port must be forwarded (it defaults to port 5173 for the UI and 3000 for its proxy server); the MCP stdio server runs as a child process of the Inspector, not as a separately exposed port; and volume mounts of node_modules are required to avoid the container's Linux packages conflicting with the host's macOS packages when running Docker Desktop.

TL;DR

Create .devcontainer/devcontainer.json referencing the mcr.microsoft.com/devcontainers/typescript-node:22 base image. Add forwardPorts: [5173, 3000] for MCP Inspector. Set postCreateCommand to pnpm install. Add the ESLint, Prettier, and TypeScript VS Code extensions. The container works as-is in GitHub Codespaces — no changes needed.

Why Dev Containers for MCP server projects

MCP server projects have a sharper "works on my machine" problem than typical web projects because the server interacts with AI clients (Claude Desktop, Cursor, Cline) that expect a specific stdio protocol. A misconfigured Node.js version, a missing global binary, or a platform-specific native module can cause the server to silently fail at the MCP handshake level — a failure mode that is much harder to debug than a broken webpage.

Dev Containers solve three specific problems for MCP development teams:

  1. Node.js version drift. The container pins Node.js 22 LTS system-wide; no version manager is needed inside the container (though fnm can still be installed for parity with local workflows).
  2. Native modules. MCP servers that use better-sqlite3, sharp, or other native addons need to be compiled for the target platform. The container's Linux platform matches the production deployment target — no macOS ↔ Linux native module conflicts.
  3. Companion services. MCP servers that front a database or message queue can declare those services in docker-compose.yml so that pg or redis is available at localhost inside the container without manual setup.

Basic devcontainer.json for a Node.js MCP server

// .devcontainer/devcontainer.json
{
  "name": "MCP Server Dev",

  // Use the Microsoft TypeScript + Node.js base image, pinned to Node.js 22
  "image": "mcr.microsoft.com/devcontainers/typescript-node:22",

  // Forward MCP Inspector's web UI and proxy ports
  "forwardPorts": [5173, 3000],
  "portsAttributes": {
    "5173": {
      "label": "MCP Inspector UI",
      "onAutoForward": "openBrowser"    // opens automatically when Inspector starts
    },
    "3000": {
      "label": "MCP Inspector Proxy",
      "onAutoForward": "silent"
    }
  },

  // Run after the container is created — install project dependencies
  "postCreateCommand": "corepack enable && corepack prepare pnpm@latest --activate && pnpm install",

  // VS Code extensions installed automatically in the container
  "customizations": {
    "vscode": {
      "extensions": [
        "dbaeumer.vscode-eslint",
        "esbenp.prettier-vscode",
        "biomejs.biome",
        "ms-vscode.vscode-typescript-next",
        "bradlc.vscode-tailwindcss",     // remove if not using Tailwind
        "github.copilot",
        "eamodio.gitlens"
      ],
      "settings": {
        "editor.defaultFormatter": "esbenp.prettier-vscode",
        "editor.formatOnSave": true,
        "typescript.tsdk": "node_modules/typescript/lib",
        "typescript.enablePromptUseWorkspaceTsdk": true
      }
    }
  },

  // Mount node_modules as a named volume to avoid host/container conflicts
  "mounts": [
    "source=mcp-server-node-modules,target=${containerWorkspaceFolder}/node_modules,type=volume"
  ]
}

The mounts entry creates a Docker named volume for node_modules. This prevents the container's Linux-compiled native modules from overwriting the host's macOS-compiled versions when using Docker Desktop with bind mounts — a common source of Error: invalid ELF header errors when switching between host and container development.

Docker Compose for MCP servers with companion services

MCP servers that front a PostgreSQL database, Redis cache, or other infrastructure benefit from a Docker Compose-based Dev Container that starts all services together.

// .devcontainer/devcontainer.json — Compose-based setup
{
  "name": "MCP Server + Postgres Dev",
  "dockerComposeFile": "docker-compose.yml",
  "service": "app",               // the service this editor attaches to
  "workspaceFolder": "/workspace",
  "forwardPorts": [5173, 3000, 5432],
  "portsAttributes": {
    "5432": { "label": "PostgreSQL", "onAutoForward": "silent" }
  },
  "postCreateCommand": "pnpm install && pnpm db:migrate",
  "customizations": {
    "vscode": {
      "extensions": [
        "dbaeumer.vscode-eslint",
        "ms-vscode.vscode-typescript-next",
        "cweijan.vscode-postgresql-client2"   // Postgres client in VS Code
      ]
    }
  }
}
# .devcontainer/docker-compose.yml
version: "3.9"
services:
  app:
    image: "mcr.microsoft.com/devcontainers/typescript-node:22"
    volumes:
      - ..:/workspace:cached          # mount the repo root
      - node-modules:/workspace/node_modules  # named volume for node_modules
    command: sleep infinity           # keep container alive; editor connects here
    environment:
      DATABASE_URL: postgres://postgres:postgres@db:5432/mcp_dev
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: mcp_dev
    volumes:
      - postgres-data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 5

volumes:
  node-modules:
  postgres-data:

Testing the MCP server from inside the Dev Container

The MCP Inspector's server command runs the MCP server as a child process inside the container. When running Inspector from inside a Dev Container (or Codespace), the forwarded port for the Inspector UI is accessible in the host browser via the forwarded URL that VS Code shows in the Ports panel.

# Inside the Dev Container terminal — start MCP Inspector
npx @modelcontextprotocol/inspector tsx src/index.ts

# The Inspector will print:
#   MCP Inspector running on http://localhost:5173
# VS Code will auto-forward port 5173 and open the browser (if onAutoForward: openBrowser)

# Or run the server standalone for testing via stdin
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"0.1.0"}}}' \
  | tsx src/index.ts

# Run tests inside the container
pnpm test

# Type check inside the container (catches Linux-specific type issues)
pnpm typecheck

GitHub Codespaces compatibility

A .devcontainer/devcontainer.json that works locally in Docker Desktop also works as a GitHub Codespace with no changes. Codespaces uses the same Dev Container spec and automatically forwards ports, provisions the container image, and runs postCreateCommand. The only Codespaces-specific consideration for MCP server development is that the GITHUB_TOKEN and other Codespace secrets are available as environment variables inside the container, which can be used as API credentials in .env files without being committed.

# .devcontainer/devcontainer.json — Codespaces-specific additions
{
  "name": "MCP Server Dev",
  "image": "mcr.microsoft.com/devcontainers/typescript-node:22",
  "forwardPorts": [5173, 3000],
  "postCreateCommand": "pnpm install",

  // Codespaces secrets available as environment variables in the container
  // Set these in the GitHub repo Settings > Codespaces > Secrets
  "secrets": {
    "GITHUB_API_TOKEN": {
      "description": "GitHub PAT for the GitHub MCP server tools"
    },
    "OPENAI_API_KEY": {
      "description": "OpenAI key if MCP server calls OpenAI"
    }
  },

  "customizations": {
    "codespaces": {
      "repositories": {
        // Permissions for the Codespace to access other repos (for multi-repo MCP)
        "your-org/mcp-core": { "permissions": "read-all" }
      }
    }
  }
}

Common failure modes

SymptomCauseFix
Error: invalid ELF header for native modulesHost-compiled macOS binaries used inside Linux containerAdd node_modules named volume mount in devcontainer.json
MCP Inspector UI not accessible in browserPort 5173 not forwardedAdd "forwardPorts": [5173, 3000] to devcontainer.json
pnpm not available in containerBase image uses npm; pnpm not pre-installedAdd corepack enable && corepack prepare pnpm@latest --activate to postCreateCommand
Database connection refused inside containerConnecting to localhost but Postgres is in a separate Docker serviceUse service name db as hostname in DATABASE_URL (e.g. postgres://...@db:5432/...)
Container rebuild on every VS Code opendevcontainer.json or Dockerfile modified — Docker rebuilds on config changeExpected behaviour; use pre-built images for faster rebuilds
VS Code extensions not installed in containerExtensions listed in local settings, not in devcontainer.jsonMove required extensions to customizations.vscode.extensions in devcontainer.json