MCP Server Security Checklist for Developers
MCP servers run with your OS privileges and can read files, call APIs, and execute code. This checklist covers the practical steps that actually matter — from installation hygiene to prompt-injection awareness.
On this page
1. Understand the threat model
The Model Context Protocol (MCP) lets AI assistants connect to external tools — databases, file systems, APIs, shells — through small server processes. Those processes are started by the MCP host (for example, Claude Desktop or a CLI wrapper) and inherit the full privileges of the OS user that launched the host. There is no built-in sandboxing layer.
This means a malicious or compromised MCP server can read any file your user account can read, write to any path your account can write, make network requests under your credentials, and spawn subprocesses. The risk profile is similar to installing an npm package that runs at startup — which is exactly what some MCP servers are.
2. Installation & trust checklist
Before adding any entry to your MCP config, check: Who published it? Does the package on npm or PyPI link to a real, maintained GitHub repository? Does the repository have a meaningful commit history, not just a one-file skeleton pushed yesterday?
A package name that mimics a popular tool (typosquatting) is a known attack vector across the Node and Python ecosystems. Verify the exact package name matches what official documentation references.
Most MCP servers are open source. Skim the entry point (commonly
index.js, server.py, or similar) and look at
which tool handlers are registered. Does the server request filesystem access?
Does it make outbound network calls? To where?
If the server is a binary you cannot inspect, treat it like any other closed-source tool: give it no more access than it strictly needs and prefer a maintained open-source alternative if one exists.
If you install an MCP server via npx some-server@latest,
the binary can change on every invocation because latest
resolves at run time. A supply-chain compromise that pushes a new patch
version will be picked up automatically.
Prefer pinning an explicit version (npx some-server@1.2.3)
or installing globally and locking the version, then upgrading deliberately
after reviewing the changelog.
3. Secrets management
MCP server configuration is typically stored in a JSON file — for example
claude_desktop_config.json on Claude Desktop. It is tempting to paste
an API key directly into that file. Do not do this.
Config files end up in backups, dotfile repositories, and cloud sync folders. A plaintext API key in a config file that gets pushed to GitHub — even briefly — is effectively compromised.
Instead, use the env block in your config to pass environment
variables to the server process. Set those variables in your shell profile or
a secrets manager, not in the config file itself.
// Bad: secret inline in config
{
"mcpServers": {
"my-api-server": {
"command": "npx",
"args": ["-y", "my-api-server"],
"env": {
"API_KEY": "sk-live-abc123HARDCODED" // ❌ never do this
}
}
}
}
// Keep secrets out of version control -- gitignore this file
{
"mcpServers": {
"my-api-server": {
"command": "npx",
"args": ["-y", "my-api-server@1.2.3"],
"env": {
"API_KEY": "sk-your-real-key" // literal value; never commit it
}
}
}
}
Note: whether your MCP host expands ${VAR} syntax depends on
the host implementation. Confirm how your specific host handles env var
injection before relying on it.
If your MCP config file lives inside a project directory that you version
with Git, add it to .gitignore immediately. The same applies
to any .env files that hold keys referenced by server processes.
# in .gitignore
claude_desktop_config.json
mcp.json
.env
*.local
If you have ever committed a config file containing an API key — even for a moment — assume it is compromised and rotate the key immediately. Git history preserves the secret even after you remove it in a later commit. Use your provider’s dashboard to issue a new key and revoke the old one.
4. Least-privilege scopes
Many MCP servers authenticate to external services using OAuth or API tokens with configurable permission scopes. Requesting broad permissions “just in case” expands the blast radius of any compromise.
When an MCP server prompts you to authorize access to a service, check which scopes it requests. A server that only reads calendar events does not need permission to send email or manage contacts. If the server requests more than it needs, that is a signal to investigate or find an alternative.
For services that support it, prefer read-only tokens when the task is read-only. A read-only token cannot delete data even if the server is compromised or sends unexpected commands.
Over time it is easy to accumulate OAuth grants to services you no longer use. Periodically review the connected apps in your accounts (for example, GitHub Settings → Applications, Google Account → Third-party access) and revoke grants for servers you have removed or stopped using.
5. Filesystem & shell server safety
Filesystem and shell MCP servers are among the most powerful and therefore most dangerous server types. They are also among the most commonly used. The following guidance applies to both official and third-party servers in this category.
Most filesystem MCP servers accept a root directory argument that restricts which paths the server can access. Pass the specific project directory you are working in rather than your home directory or the drive root.
// Narrow scope — only the current project
"args": ["C:\\Users\\you\\projects\\my-project"]
// Too broad — exposes everything
"args": ["C:\\Users\\you"] // ❌ includes SSH keys, browser profiles, etc.
Directories to avoid passing as a filesystem server root include:
~/.ssh— SSH private keys~/.aws,~/.azure,~/.config/gcloud— cloud credentials- Browser profile directories — stored passwords and session cookies
- Password manager data directories
- Any directory that contains
.envfiles with secrets
If your project directory happens to contain a .env file,
make sure the MCP server you are using does not read or transmit it.
A shell execution server can run any command the model sends it. This means that a single bad model output — whether from a mistake, a jailbreak, or a prompt-injection attack — can delete files, exfiltrate data, or install software.
If you use a shell server, consider restricting it to a specific allow-list of commands if the server supports that, running it inside a container or virtual machine, and never leaving it enabled in your config when you are not actively using it.
6. Building your own MCP servers
If you are developing a custom MCP server, you control the attack surface. These practices reduce the risk that your server becomes a vulnerability.
Your tool handler receives arguments chosen by the model. The model can be manipulated (see prompt injection below) into sending unexpected values. Validate argument types, lengths, and formats before using them.
If an argument is used to construct a file path, check that the resolved
path stays within the intended root directory — a naive
implementation can be bypassed with ../../../etc/passwd style
traversal. Use your language’s path normalization and comparison
functions to enforce boundaries.
// Node.js example — path traversal guard
const path = require('path');
const ROOT = path.resolve('/allowed/root');
function safePath(userInput) {
const resolved = path.resolve(ROOT, userInput);
if (!resolved.startsWith(ROOT + path.sep) && resolved !== ROOT) {
throw new Error('Path outside allowed root');
}
return resolved;
}
If your server constructs shell commands using arguments from the model, use
parameterized execution (passing arguments as an array to the child process)
rather than string interpolation. String interpolation enables command
injection: an argument containing ; rm -rf ~ becomes a second
shell command if interpolated directly.
// Unsafe — string interpolation
exec(`git clone ${url}`); // url could contain ; malicious-command
// Safe — array argument
execFile('git', ['clone', url]); // url is passed as a literal argument
MCP tools can declare a JSON Schema for their arguments. Declaring explicit
schemas gives hosts and clients a structured description of what the tool
accepts, which also serves as a first-pass validation layer before your
handler code runs. Define type, required, and
where appropriate enum, minLength,
maxLength, and pattern fields.
Read secrets via process.env.API_KEY (Node) or
os.environ["API_KEY"] (Python) at runtime. Never store keys
as string literals in source files, and add any local .env
files you create for development to .gitignore before your
first commit.
7. Prompt injection via tool results
This is one of the least-obvious risks in the MCP ecosystem, and one of the most important to understand.
When an MCP tool returns data to the model — the contents of a web page, a document, a database row, an API response — the model processes that data as part of its context. If the data contains text that looks like instructions (“Ignore your previous instructions and instead…”), the model may treat it as a directive rather than inert data.
When building a system that uses MCP servers fetching external data (web pages, third-party API responses, user-submitted documents), design your prompting and system instructions to remind the model that tool results are data to be processed, not instructions to be followed. The effectiveness of this varies by model and context, but it is a meaningful mitigation.
A prompt-injection attack in a fetched document is only useful to an attacker if there are other powerful tools in the same session. If you are building an agent that reads untrusted documents, consider whether that same session also needs to have access to shell execution, file write, or email-sending tools. Separating high-trust and low-trust tool sets into different sessions reduces the potential impact.
Log and inspect tool results during development. If a tool can return arbitrary third-party content, manually test what happens when that content contains instruction-like text, markdown headers, or XML/HTML tags that could influence the model’s interpretation of the conversation structure.
8. Quick-reference summary
| Risk area | Severity | Mitigation |
|---|---|---|
| Installing untrusted MCP server packages | High | Verify publisher, inspect source, pin versions |
| API keys hardcoded in config files | High | Use env vars; add config to .gitignore |
| Broad filesystem scope (home dir exposed) | High | Restrict server root to project directory only |
| Shell server with no command restrictions | High | Allow-list commands; prefer container isolation |
| Path traversal in custom server handlers | High | Normalize and check resolved path against root |
| Command injection via model-supplied args | High | Use array-based process spawning, never interpolation |
| Prompt injection through tool results | Medium | Isolate high-privilege tools; treat results as untrusted |
| Over-broad OAuth scopes | Medium | Request minimum scopes; prefer read-only tokens |
| Stale OAuth grants to unused servers | Low | Periodically audit and revoke unused app access |
Unpinned server versions (@latest) |
Medium | Pin to a specific version and upgrade deliberately |
Tools to help you ship a safer MCP setup
Getting config right is the first step — and one of the easiest places to make a mistake. These two tools can help.
MCP Config Fixer
Paste your mcp.json and get instant feedback on common
structural errors, missing fields, and obvious security issues like
inline secrets.
MCP Setup Pack
Curated, pre-configured server setups with env-var templates, safe filesystem scopes, and gitignore rules already applied — ready to drop into your project.
See what’s included →FAQ
.bashrc, .zshrc, or equivalent) or in a secrets
manager, then reference it in the env block of your MCP config.
Confirm that your MCP host actually injects environment variables into the
server process — the mechanism varies by host implementation.
Never write the key as a literal string in the config file itself.
.gitignore
and consider using a tool like BFG Repo-Cleaner or
git filter-repo to rewrite history if the repository is private
and you need to clean it up.