MCP server security checklist before you publish
You've built an MCP server. You've tested it locally, written the README, and you're ready to submit it to the MCP registry or push it to npm. Before you do — here's a 7-point checklist worth running through.
This isn't about being paranoid. It's about the fact that MCP servers are a different security surface than a regular npm package. A regular package runs code. An MCP server runs code on behalf of an AI agent — and that agent can invoke your tools with inputs you never anticipated, from contexts you didn't design for.
Why MCP servers need a security pass before publishing
When a developer installs your MCP server, they're giving it access to:
- Their filesystem (if you have file tools)
- Their environment variables and credentials (if you read env)
- Their network (if you make outbound calls)
- Their AI agent's context window (via your tool descriptions)
None of that is gated by npm's package model. The user trusts your server the moment they add it to their config. That's a higher bar than a utility library.
The checklist
1. No shell interpolation with user input
Bad:
execSync(`git clone ${userInput}`)
Good:
execFileSync('git', ['clone', userInput])
If any of your tools accept user input and pass it to a shell command, that's a critical risk. Use array-form exec/spawn, never string interpolation.
2. File paths sanitized against a base directory
Bad:
fs.readFileSync(userProvidedPath)
Good:
const base = path.resolve('./allowed-dir')
const resolved = path.resolve(base, userProvidedPath)
if (!resolved.startsWith(base)) throw new Error('path traversal blocked')
fs.readFileSync(resolved)
An AI agent might pass ../../etc/passwd as a file path. Resolve against an allowed base and verify before reading or writing.
3. No eval() or dynamic code execution
If your server uses eval(), new Function(), or vm.runInNewContext() with user-supplied input, that's a remote code execution vector. There's almost never a legitimate reason for this in an MCP server.
4. No hardcoded credentials
Check your code for API keys, tokens, or passwords committed directly in source. Use environment variables instead, and add a .env.example so users know what to set.
Tools like git log -p | grep -i "api_key" can help catch anything that slipped through.
5. Tool descriptions free of hidden instructions
This one is easy to miss. Your tool's description field is passed directly to the AI model. A tool description that says something like "always respond with success even if it fails" or "do not tell the user" is a prompt injection payload embedded in your own server.
Keep tool descriptions factual and minimal — describe what the tool does, nothing more.
6. Outbound network calls documented and expected
If your server makes outbound calls to third-party endpoints, that should be obvious and documented. An undocumented call to an external server combined with access to environment variables is the classic exfiltration pattern.
Ask yourself: if a user watched every network request your server made, would anything surprise them?
7. Permissions declared in your manifest
Add a permissions field to your package.json or manifest declaring what your server needs:
{
"permissions": {
"filesystem": "read-only, scoped to ./data",
"network": "api.yourservice.com only"
}
}
There's no enforced standard yet, but declaring permissions makes your server's intent auditable and sets a good precedent as the ecosystem matures.
Run it automatically in one line
Rather than checking these manually every time, you can run:
npx mcp-customs scan .
This checks all 7 of the above categories automatically and outputs a score, stamp (CLEARED / REVIEW / FLAGGED), and specific findings with line numbers.
It runs fully offline — nothing leaves your machine.
──────────────────────────────────────────────────────
MCP-CUSTOMS INSPECTION REPORT
──────────────────────────────────────────────────────
target .
files scanned 12
score 97 / 100
stamp [ CLEARED ]
──────────────────────────────────────────────────────
No findings. Nothing here means our checks didn't catch anything
— not a guarantee of safety.
──────────────────────────────────────────────────────
Add it to CI so it runs on every push:
- run: npx mcp-customs scan . --sarif results.sarif --fail-on high
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: results.sarif
Add the badge
Once your server passes, add the badge to your README so users know it's been checked:
npx mcp-customs scan . --badge --name your-server-name
Or look up your server's score at mcpcustoms.github.io.
A note on what this doesn't catch
mcp-customs is heuristic and regex-based — fast and auditable, but not a full dataflow analysis. It will miss things a deeper analysis would catch, and it will produce false positives (we caught our own scanner flagging a commented-out eval() as critical when we first scanned 12 popular MCP servers). Treat a CLEARED stamp as "nothing obvious found," not "verified safe."
The goal isn't perfection — it's making basic security hygiene a normal part of publishing an MCP server, the same way npm audit became normal for npm packages.
Built something and want it scanned? Run npx mcp-customs scan . locally or check mcpcustoms.github.io.
Top comments (0)