pickuma.
AI & Dev Tools

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.

7 min read

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

Try OpenCode

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?
For stdio servers, yes. The client spawns the server as a child process under your user account, so it inherits your UID and can reach any file that account can reach. Per-directory restrictions advertised by a server are enforced by that server's own code, not by the operating system.
Are remote HTTP MCP servers safer than local stdio ones?
They trade one risk for another. A remote server cannot read your filesystem directly, but everything the agent sends it leaves your machine and lands with a third party, and you cannot inspect the process with lsof or strace. Audit remote servers by what data the agent passes them and by the OAuth scopes you granted, not by process inspection.
How do I tell whether a server changed its tools after I approved it?
Save the tool list your client reports — names, descriptions, and schemas — when you first approve a server, then diff it after updates. Pinning the package version removes most of this problem, because the tool definitions cannot change unless the pinned version does.

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

See all AI & Dev Tools articles →

Get the best tools, weekly

One email every Friday. No spam, unsubscribe anytime.