An MCP server is not a plugin in any sandboxed sense of the word. When your editor or agent starts a local one over stdio, it spawns a child process that runs as you: same UID, same home directory, same reachable ~/.ssh, ~/.aws/credentials, and .env files, same outbound network. The Model Context Protocol specifies how the client and server talk. It does not specify what the server is allowed to touch, and no mainstream client sandboxes stdio servers by default.
That is manageable with one server you wrote yourself. It stops being manageable at five servers pulled from npm, three of which you installed because a README said npx -y. The audit below takes about ten minutes and tells you, per server, which files it opened, which hosts it dialed, and which of your environment variables it inherited.
What a stdio server inherits at launch
Two transports matter in practice. A stdio server is a subprocess: the client launches your configured command with args and exchanges JSON-RPC over the pipe. An HTTP server (Streamable HTTP, or the older HTTP+SSE variant) is a remote endpoint the client calls over the network. The threat models are different — the stdio one is local privilege, the HTTP one is data egress and third-party trust — and most setups mix both without separating them.
For the stdio case, four things get handed over at spawn time.
The process identity. The server runs under your account. Anything you can read, it can read. Anything you can delete, it can delete. Flags like --allowed-directory ~/projects are enforced by the server's own code, not by the OS, so they hold exactly as long as that code is correct and unbypassed.
The environment. Clients differ in how much of your shell environment they forward, so do not guess. On macOS, ps eww <pid> prints the environment of a process you own. On Linux, tr '\0' '\n' < /proc/<pid>/environ does the same. If AWS_SECRET_ACCESS_KEY or GITHUB_TOKEN shows up in that dump for a server that only needs to read Markdown, that is your finding.
Secrets written into config. The env block in .mcp.json, ~/.cursor/mcp.json, or claude_desktop_config.json stores API keys as plain text. Run ls -l on those paths. If any of them is group- or world-readable, or sits inside a repo you push, fix that before anything else here.
Whatever the package resolves to today. npx -y some-mcp-server fetches and executes the current published version at every launch. You approved the code you read last month; you are running whatever shipped this morning.
The
-yflag suppresses the install prompt, and an unpinned package name resolves tolatest. A server you audited in March can publish new code in August and run it against your credentials before you notice the version moved. Pin it:npx -y pkg@1.4.2, oruvx pkg==1.4.2for Python servers.
Four commands that show what a server actually reaches
Start your client, let the servers come up, then work through these one PID at a time. claude mcp list (or the /mcp panel in-session) tells you which servers your client thinks are running. ps tells you what is running.
# 1. What is alive, under which user, with what argv
ps -eo pid,ppid,user,etime,args | grep -i -E 'mcp|modelcontext' | grep -v grep
# 2. Open files, sockets, and working directory for one server
lsof -p <pid>
# 3. Network only: who is it talking to
lsof -nP -i -a -p <pid>
# 4. Live filesystem access while you exercise a tool (macOS)
sudo fs_usage -w -f filesys <pid>
# Linux equivalent
strace -f -e trace=openat,connect -p <pid>
Read the output in that order. Step 1 surfaces servers you forgot you installed, and the PPID column matters: a wrapper script that launches a second process means the second process is the one worth inspecting. Step 2 gives you cwd plus every open descriptor — a filesystem server scoped to one project should not be holding handles in ~/Library or ~/.config. Step 3 is the step people skip. A server that presents itself as purely local should show no established outbound connections at all, and one unexplained TLS session to a host you do not recognize is worth chasing before you use that server again.
Step 4 turns a snapshot into evidence. Attach fs_usage or strace, trigger a single tool call from the agent, and watch what the process opens. That is the difference between "the README says it only reads the workspace" and knowing that it only read the workspace.
An MCP server supplies its own tool names, descriptions, and JSON schemas, and your client injects that text straight into the model's context. Security researchers have demonstrated that instructions hidden in tool descriptions can steer an agent into reading files or leaking data the user never approved, and that a server can present a benign description at install time and change it later. Diff the tool list your client shows against what you approved, and treat an unexplained change the way you would treat a changed lockfile hash.
Containment that survives the next update
An audit describes today. These three changes keep holding when the package moves underneath you.
Pin versions and put them in review. Replace every unpinned npx -y pkg with an exact version. For servers you use daily, install into a project-local node_modules and point command at the resolved binary path, so the version lands in your lockfile and shows up in a pull request instead of only in someone's dotfiles.
Give the process less than you have. On Linux, bwrap with an explicit --ro-bind list, or a container started with --network none and one read-only mount, gives you a boundary the kernel enforces rather than one the server enforces on itself. macOS is thinner here — sandbox-exec still functions and is still deprecated — so the practical fallback is a dedicated non-admin account, or a VM, for anything you do not fully trust.
Scope credentials per server. A GitHub MCP server needs a fine-grained token limited to specific repositories, not your personal access token carrying repo across everything you can see. When a server is compromised, or just buggy, the blast radius equals what you handed it — the one variable entirely under your control.
If you only do one of the three, do the credentials. Scoped tokens cap the damage whether or not the audit caught the problem, and they cost about five minutes per service.
Originally published at pickuma.com. Subscribe to the RSS or follow @pickuma.bsky.social for new reviews.
Top comments (0)