DEV Community

yukihiro amadatsu
yukihiro amadatsu

Posted on

Your Claude Code MCP servers are quietly costing you gigabytes of RAM

日本語版は Zenn にあります: Claude Code の MCP サーバー、実はメモリ数GB食ってますよ

The .mcp.json you probably have

If you've wired MCP servers into Claude Code (or Cursor), yours probably looks
like this:

{ "mcpServers": { "terraform": {
    "command": "docker",
    "args": ["run", "-i", "--rm", "hashicorp/terraform-mcp-server"] } } }
Enter fullscreen mode Exit fullscreen mode

That docker means Docker Desktop is running — resident, all day — to host
this. Open Activity Monitor and look: "Virtual Machine Service for Docker"
alone is over 1 GB, plus the backend and UI processes around it.

Now consider what an MCP server actually does. It only exists during a session,
and even then it's idle ~99% of the time, waiting for a tool call. You're
keeping a gigabyte-class VM warm around the clock for a tool shed that's
almost always asleep.

On macOS 26 you can take that memory back with a one-line swap to Apple's
first-party container runtime:

claude mcp add terraform -- container run -i --rm hashicorp/terraform-mcp-server
Enter fullscreen mode Exit fullscreen mode

That's the whole migration. No Docker Desktop, no extra tooling. I verified
this with Docker Desktop fully quit — zero docker processes alive — and
Claude Code tool calls still going through.

(If this is your first time with Apple's container, install and start the
runtime first — brew install container, then container system start.
Details in the migration section below.)

The rest of this post covers why this workload is the one place Apple
container genuinely wins (I have measurements), where you'll outgrow the raw
command, and the one trap I hit running it for real.

MCP servers are the shape Apple container is good at

In my previous post
I measured Apple container and concluded, honestly, that it's not ready
to be your daily dev environment: every container gets its own VM at ~270–400
MB, and throwaway containers start 4–10× slower than Docker's. For a
multi-service dev stack, those numbers hurt.

But an MCP server inverts every one of those trade-offs:

  • a few small images, not a stack
  • alive only during a session, idle nearly all of it
  • third-party code holding your API tokens — the more isolation, the better

Apple container has no resident VM. Idle host-side cost is roughly 50 MB,
and the MCP server's VM exists only while your session does. Docker Desktop's
shared VM runs 1.4–2.1 GB all the time for the same job. For a tool shed
that's 99% idle, that's a bad rent.

And per-VM isolation is not just a talking point here. An MCP server is
somebody else's code holding your GitHub token. On Apple container, each
server gets its own VM — a materially stronger wall than Docker containers
sharing one kernel.

Migrating (about a minute)

Prerequisites: macOS 26 on Apple silicon, plus one-time runtime setup.

brew install container   # or grab it from github.com/apple/container/releases
container system start
Enter fullscreen mode Exit fullscreen mode

Then swap the registration:

claude mcp remove terraform
claude mcp add terraform -- container run -i --rm hashicorp/terraform-mcp-server
Enter fullscreen mode Exit fullscreen mode

Open a new session, check /mcp shows connected, call a tool once. Images are
pulled automatically on first connect.

After quitting Docker Desktop, Activity Monitor shows the Docker family
(backend + VM + UI, over 1 GB combined) gone. In its place: one "Virtual
Machine Service for container-runtime-linux" at ~280 MB and a ~20 MB helper —
and only while a session is open.

Where the raw command stops being enough

A bare container run covers exactly one case: a single stdio server with no
secrets. Hit any of the following and it's time to graduate to a compose file.
I'll use opossum, the compose-compatible
orchestrator for Apple container I've been building, but the reasoning is
tool-agnostic.

1. A server that needs a token

With raw commands, the token ends up inline in .mcp.json — a file that's
often project-scoped and committed. That's an incident waiting to happen. With
compose, the token lives in a git-ignored .env, and .mcp.json carries only
a command:

// .mcp.json — no token in sight
{ "mcpServers": { "github": {
    "command": "opossum",
    "args": ["-f", "/path/to/mcp-stack/compose.yaml", "run", "--rm", "github"] } } }
Enter fullscreen mode Exit fullscreen mode
# compose.yaml — the token is injected from .env next to this file
  github:
    image: ghcr.io/github/github-mcp-server
    environment:
      GITHUB_PERSONAL_ACCESS_TOKEN: ${GITHUB_TOKEN:-}
    profiles: ["stdio"]   # not started by `up`; runs only when invoked
Enter fullscreen mode Exit fullscreen mode

.env resolves relative to the compose file, so it works no matter which
directory your MCP client launches from.

2. Several servers

At three or four servers you start wanting bulk image updates
(opossum pull), a single config listing (opossum config), and one file
that is your toolbox.

3. An HTTP-transport server

Streamable-HTTP MCP servers are long-running services — that's up / down /
logs territory, not run --rm:

opossum -f mcp-stack/compose.yaml up
Enter fullscreen mode Exit fullscreen mode
// .mcp.json just points at the URL
{ "mcpServers": { "terraform-http": { "url": "http://localhost:8080/mcp" } } }
Enter fullscreen mode Exit fullscreen mode

One gotcha: if the server binds its default 127.0.0.1, the published port
can't reach it. Bind all interfaces — e.g. --transport-host 0.0.0.0 for
terraform-mcp-server; other servers have an equivalent flag.

A working example with all three patterns in one file is at
examples/mcp-stack.

The trap I actually hit

"Connected, but every tool call fails." A stdio connection runs over
stdin/stdout and never touches the network, so the connection looks perfectly
healthy even when the container's outbound networking is dead. Tools fail only
at the moment they hit an external API — and the error comes back wearing an
innocent face like "provider not found", nothing that says network.

In my case, outbound traffic on the default network died after the runtime had
been up for several days. The triage is one line:

container run --rm alpine ping -c1 1.1.1.1
Enter fullscreen mode Exit fullscreen mode

If that fails, container system stop && container system start recovers it.
If you use opossum, opossum doctor runs this probe as part of a one-shot
environment check.

Honest caveats

  • Session start pays ~1 second of VM boot. Tool calls after connection are unaffected.
  • Xcode users get first-party MCP integration (Xcode 27 supports MCP natively). This post is for people who live outside Xcode — Claude Code, Cursor, and friends.
  • I still don't recommend migrating your dev stack itself. The measurements in the previous post stand. MCP servers are the first real-world instance of the one shape that post said Apple container is good at: small, occasional, idle.

Summary

  • On macOS 26, changing docker to container in .mcp.json runs your MCP servers without Docker Desktop
  • The MCP workload (small, idle, credential-holding) matches Apple container's strengths exactly
  • Graduate to compose when you hit tokens, server sprawl, or HTTP transport — until then the raw command is enough
  • If tools fail while showing connected, ping 1.1.1.1 from a container first

Agent toolboxes only grow from here, and the shed they live in should be light
and isolated. Models keep getting smarter on their own — the leverage is in
the tooling around them, and there's no reason that tooling should cost you
gigabytes of always-on RAM.

If you run your MCP setup on Apple container, I'd love to hear how it went —
comments or issues both work.
"Here's what happened with my config" is plenty.

Top comments (0)