MCP Server Security: How to Audit What a Local MCP Server Can Actually Reach
A local MCP server runs as a child process with your user's permissions. Four commands to see which files it opens and hosts it dials, plus three containment changes that survive the next package update.
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.
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.
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.
OpenCode
Terminal coding agent whose MCP server definitions live in a config file you can commit, so version pins and new servers go through the same review as the rest of your code.
Free tier available, then per-token billing
Affiliate link · We earn a commission at no cost to you.
FAQ
Does a local MCP server really run with the same permissions as me?
Are remote HTTP MCP servers safer than local stdio ones?
How do I tell whether a server changed its tools after I approved it?
Tools used in this review
Some links above are affiliate links. We may earn a commission if you sign up. See our disclosure for details.
Related reading
2026-08-12
Spec-Driven Development With AI Agents: Writing a Spec an Agent Can Actually Execute
How to structure a spec so a coding agent can run it end to end without babysitting: ground truth files, an interface contract, one acceptance command, and explicit out-of-bounds rules.
2026-07-16
How to Measure Cost Per Task with Terminal AI Agents
Terminal AI agents like OpenCode and Claude Code bill by the token, which makes cost per task the metric that matters. Here is how we tracked it and what we learned.
2026-07-16
Multi-Agent Terminal Workflows: Combining OpenCode, Claude Code, and Aider
Running more than one AI coding agent in your terminal is becoming a real workflow. Here is how we split work between OpenCode, Claude Code, and Aider without losing track of the changes.
2026-07-16
OpenCode First Project Setup: From Install to First Passing Test
A practical setup guide for OpenCode on an existing project. Configure providers, context files, and project conventions so the agent produces useful output on day one.
2026-07-16
Running OpenCode with Local LLMs for Private AI Coding
You do not need to send source code to a cloud API to use an AI coding agent. Here is how we ran OpenCode against a local Ollama model for a proprietary codebase.
Get the best tools, weekly
One email every Friday. No spam, unsubscribe anytime.