DEV Community

mcpcustoms
mcpcustoms

Posted on

MCP server security checklist before you publish

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}`)
Enter fullscreen mode Exit fullscreen mode

Good:

execFileSync('git', ['clone', userInput])
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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 .
Enter fullscreen mode Exit fullscreen mode

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.
──────────────────────────────────────────────────────
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)