MCP security: what a server can reach, and how to bound it
Written while shipping a computer-use MCP server. Every control below is one we had to implement, and each has a test that was broken on purpose to prove it can fail.
MCP security is the practice of bounding what a Model Context Protocol server can reach, and making what it did visible afterwards. The protocol grants no sandbox. An MCP server is an ordinary program your agent is allowed to call, running as you, with your files and your logged-in sessions. Every guarantee comes from the individual server, never from MCP itself.
Three questions decide whether a given server is safe to leave running: what can it reach, who approves an action before it happens, and where is the record.
The risks, as they actually occur
The tool list is the permission list
This is the whole of it, and it is easy to miss because a tool list reads like documentation rather than a grant. A server with a run_command tool has code execution. A server with read_file has your .env, your SSH keys and your browser profile. A server that fetches URLs can move data off the machine.
None of these are bugs. They are features, correctly implemented, and they are why the careful answer to "is MCP safe" is always "which server?"
Credentials arrive in screenshots without anyone deciding to share them
A browser autofills a password into a login form. An agent takes a screenshot to see what happened. The credential is now in an image, in the model's context, and possibly in whatever logs that conversation. Nobody chose this; it is a side effect of two reasonable behaviours meeting.
The mitigation named in the security literature is redaction at capture time. On macOS the signal is there for the taking: native secure fields carry the role AXSecureTextField, while a password field inside a web page carries role AXTextField with the subrole AXSecureTextField. Checking only the role catches native fields and lets every browser password box through.
Prompt injection turns reading into doing
Any content an agent reads can contain instructions: a web page, an issue comment, a file. When the agent also holds tools that write, the distance between "read this page" and "run this command" is one convincing paragraph.
No server can make this impossible. What a server can do is make it visible: require a human decision before the first write, and record every call so the sequence can be reconstructed afterwards.
An audit log full of credentials is its own breach
The obvious way to log a keystroke tool is to log the keystrokes. Do that and the log becomes the highest-value file on the machine, in plain text, exempt from every control that protects the thing it recorded.
Consent that resolves to yes when nobody answers
A prompt that times out into approval is not consent, it is a delay. The only defensible default for an unanswered dialog is refusal, even though that means an unattended run stops.
An MCP security checklist
Run this against each server you have installed. The answers come from the tool list and the README; where a server cannot answer a question, that is itself the answer.
- Can it execute a shell command? Look for any tool that runs, executes, spawns or evaluates. One such tool makes every other question secondary.
- Can it read arbitrary files, or fetch arbitrary URLs? Either one turns a local capability into a data path off the machine.
- What is the blast radius of one confused call? A screenshot is recoverable. A keystroke into a terminal is not.
- Does any action require a human to approve it? If so: once per session, or once per action, and which actions are exempt?
- What happens when nobody answers the prompt? If an unanswered prompt proceeds, there is no consent gate, only a delay.
- Is there a log, and can it be edited? Append-only with restrictive permissions, or it is a convenience rather than a record.
- Does the log store typed text in clear? A length and a hash prove two actions typed the same thing without storing what it was - and the hash must be salted, or a short password can be guessed offline by whoever holds the log.
- Does it send anything off the machine? "No telemetry" is a claim; an empty grep for network calls is evidence.
- Where do its credentials live at rest? A token in a config file is readable by every process running as you.
- What does it do that you did not ask for? Update checks, crash reporting, analytics. Each is a connection you did not choose.
The honest ordering. Questions 1 and 2 dominate. A server that can run a shell makes the remaining eight academic, because any answer it gives can be changed by the shell it exposes.
MCP server security best practices
| Practice | Why it holds |
|---|---|
| Grant capability, not access | Expose the narrowest tool that does the job. "Press the element named X" is bounded in a way "click at 812, 466" is not, and neither is bounded the way a shell is unbounded. |
| Refuse ambiguity | When two elements match, returning the candidates beats picking the first. Pressing a plausible-looking button is the kind of almost-right action nobody notices until later. |
| Fail closed | Timeouts, missing permissions and unparseable input should all refuse. The failure mode of a security control must be "nothing happened". |
| Keep a list that always asks | Password managers and terminals should prompt every time, even after a session is approved, and that list should not be configurable away. It is the difference between "the agent may work" and "the agent may fetch my passwords". |
| Redact before writing, not after | If an unredacted file exists for a moment, it exists. Paint the bitmap in memory, then write. |
| Fingerprint, and salt it | Store a length and a salted hash prefix of typed text. Enough to prove two actions matched, never enough to read - and a plain fingerprint is not enough, because twelve characters are already enough to confirm a guess against a short password. |
| Say who did it | One machine runs several agents. Without a per-session mark, a shared log answers what happened but not which conversation did it. |
| Test the control, then break it | A security test that has never been seen to fail may be asserting nothing. Change the code on purpose; if the test stays green, the test is decoration. |
| Write down what you do not cover | Every control has an edge. A page that lists only strengths is read as marketing and trusted accordingly. |
How this server answers each one
Not as a claim - as a file you can read and a test you can run.
| Control | Implementation | Test |
|---|---|---|
| No shell, no file access, no URL fetching | Absent by design. Each would be a few lines. | claims.mjs claim 4 scans the tool list |
| Secure fields painted out in memory | Capture, find rects, paint, write - in that order, so no unredacted file exists | redaction-unit.py, four checks |
| Role and subrole checked | Native and in-page password fields are detected differently | same suite |
| Nothing writes until a human agrees | Session consent; password managers and terminals ask every time, also in allow mode | claims.mjs claims 1 and 1b |
| Unanswered prompt refuses | Timeout resolves to no | failclosed.mjs |
| The gate judges the named app | A press names its target, so the always-ask list sees the real app rather than whatever is frontmost | claims.mjs claim 5 |
| Append-only log, mode 0600 | Every call, its target, allowed or refused, with the reason | server-e2e.mjs |
| Typed text never in clear | Length plus a salted SHA-256 prefix. The salt is random per run and never written down | claims.mjs claims 2, 2b and 2c |
| Several agents stay distinguishable | Per-server session mark, plus a name from CMCP_CLIENT | concurrent.mjs: two servers, 50 interleaved writes, zero torn lines |
| Nothing leaves the machine | No network calls in the product; zero third-party dependencies in the privileged binary | small enough to read in one sitting |
Each of those tests has been mutation-checked: the code was broken on purpose and the test went red. One of them did not, the first time. It only checked that the call failed, so it passed with the consent gate removed entirely - the call had failed for an unrelated reason. That test was rewritten to require the gate's own words.
What none of this covers
Consent is not containment. After you approve, the agent drives your real machine. That is what you approved. If you need a boundary rather than a decision, run it in a virtual machine.
Only what the system marks as secure can be redacted. A password in a plain text editor, or a token printed into a terminal buffer, carries no marker and will not be hidden.
Prompt injection stays possible. The prompts and the log make it visible rather than silent. They do not make it impossible.
Unattended running has a real ceiling. Because an unanswered prompt refuses, a long run stops when it reaches a terminal or a password manager. That is the promise working rather than a defect - but it means "leave it running for days" is only true for read-only work and for apps outside the always-ask list.
Mapped against the published taxonomy
This checklist is practical rather than exhaustive. OWASP has a numbered list - the OWASP MCP Top 10 - and we walk all ten categories, marking the four where the honest answer is "not our layer" or "this one cannot be closed by any MCP server".
Frequently asked
Is MCP itself insecure?
MCP is a protocol for describing tools. It has no opinion about what a tool may do, which is why the answer to "are MCP servers safe" is always about the individual server. The protocol is not the control surface; the server is.
Does an MCP server need an API key?
Some do, and each key is a credential sitting in a config file readable by everything running as you. A server that needs no account and holds no key removes that question entirely.
Can I just run everything in readonly?
For research and for the first hour, yes, and it is the right default. A read-only server's worst case is that it saw something - which is exactly why redaction still matters in readonly.
How do I audit what an agent actually did?
Read the server's log, not the agent's summary. A model describes what it intended; the log records what was called. Where those disagree, the log is right.
Read next
The full security model - the three gates in detail, with the code paths.
Your agent's screenshot tool is reading your passwords - the redaction race, measured.
Every tool this server exposes - the permission list, in full.