DEV Community

Umang Kumar
Umang Kumar

Posted on

Securing MCP servers: a practical checklist for 2026

MCP servers look harmless because they expose tools, not endpoints. A client calls a tool and gets a result. But the tools often touch a filesystem, a database, a package manager, or a deployment API. That makes the MCP server the authorization boundary, whether you planned it or not.

Here are the five failure modes I've seen show up in production and a short checklist you can run through before exposing a server to agents.

Tool-definition drift

The client decides what a tool does by reading its declaration: name, description, and parameter schema. If the declaration lies, or drifts from what the handler actually does, the client's policy is enforcing the wrong thing.

Real examples: a tool declared as "read a file at this path" that silently tails a log stream; a "list packages" tool whose handler also accepts a --resolve flag that performs installs; a parameter added server-side that the client's declaration doesn't mention yet the agent keeps sending.

Mitigation: the client should pin the tool schema it authorized (hash it) and reject handlers whose live declaration diverges. Server-side, version the tool manifest and log every declaration change. Treat a declaration update the same way you treat a dependency bump — it's an authority surface.

Over-broad filesystem tools

A filesystem tool that accepts any path is a filesystem tool that accepts /etc/shadow, ~/.ssh, and every mounted secret. I've seen MCP servers ship a single read_file(path) and a single write_file(path, content) and then rely on the client to pass safe paths.

Clients are agents. Agents are given goals. If the goal is "fix this deploy" and the agent has a write_file that accepts any path, the shortest path to the goal sometimes writes to /etc or to a directory that the CI pipeline reads from.

Narrow the tools. Ship read_project_file(glob) scoped to the repo root, write_project_file(glob, content) scoped the same way, and a separate, explicitly-named tool for anything outside the project. Make the default "no" for the rest of the filesystem.

Secrets in the environment

MCP servers run as processes. Processes inherit environment variables. It is common to run an MCP server inside a container or a CI step that also carries DATABASE_URL, AWS_ACCESS_KEY_ID, or a signing key.

An agent with a "run command" tool or a filesystem tool can read /proc/self/environ or list the directory a shell writes .env files to. The server didn't ship a "read secrets" tool, but the tools it did ship composed into one.

Drop sensitive variables at the server boundary, not at the agent boundary. Run the MCP server with a minimal environment and pass only what its tools need. If a tool needs a credential, pass it through the server's own secret store, not through the process environment the agent can observe.

Egress allowlists

A tool that fetches a URL, installs a package, or calls a registry can turn an MCP server into a data exfiltration or supply-chain hop. The client authorized the tool; the tool's network destination is what determines the blast radius.

Treat outbound connections from the MCP server like outbound connections from any service that runs untrusted input: an allowlist of destinations, and nothing else. Package installs go to the pinned registry. API calls go to the declared host. Everything else is dropped.

You don't need a full egress proxy to get started. Even iptables rules or a container runtime's network policy on the MCP server process surface is enough to make the difference between "the agent fetched a template" and "the agent phoned home with a config file."

Audit logging

You cannot defend what you cannot replay. Every tool invocation the server executes should be logged with: the calling client identity, the tool name, the input parameters (redacted for credentials), the outcome, and a server-side timestamp.

Logs should be append-only after the fact and shipped out of the host the server runs on. If an agent has a filesystem tool on that host, it has a write tool on the log file.

Checklist

Run this against every MCP server you expose to an agent before you let it run workloads:

  • [ ] Pin and hash each tool declaration on the client; reject handlers that drift.
  • [ ] Replace blanket filesystem tools with path/glob-scoped tools rooted to the project.
  • [ ] Strip secrets from the server's process environment; pass credentials only through the server's own store.
  • [ ] Apply an egress allowlist to the server's outbound connections (registry + declared API hosts only).
  • [ ] Log every tool invocation with caller identity, tool, redacted inputs, outcome, and server timestamp; ship logs off-host.
  • [ ] Review the server's tool manifest after every update the same way you review a dependency bump.

MCP servers aren't untrusted in the browser sense. They're more like a CI runner that an agent can steer. Secure them like one.

I'm building Cirvix AgentControl, an open-source default-deny policy layer for agent tool calls: https://github.com/CIRVIX/agent-control (try npx @cirvix_ai/agent-control scan).

Top comments (5)

Collapse
 
max_quimby profile image
Max Quimby •

The /proc/self/environ point is the one that bites teams who think they shipped a "safe" server. You can audit every tool you declared and still lose, because a run_command tool plus an inherited DATABASE_URL compose into a secrets-read tool you never wrote. We learned to treat the server process env as hostile: strip sensitive vars at the boundary and pass the few the handlers actually need explicitly, rather than letting the container's ambient env leak through.

The schema-pinning idea is underrated too. We hash the tool manifest a client authorized and reject on divergence, because "the description says read, the handler also writes" is almost never caught at review time — it shows up when an agent takes the shortest path to a goal. One question: how do you handle legitimate manifest updates without a human in the loop on every bump? We've ended up treating a declaration change exactly like a dependency bump — pinned, logged, and re-approved — but it does add friction.

Collapse
 
umangcirvix profile image
Umang Kumar •

Treating a manifest change like a dependency bump is the right default, and I don't think it should get looser. Where you can cut friction is in what gets re-approved: a description-only change can be logged and auto-accepted, but a new tool or a widened input schema should go to a human. That's how I'm thinking about pinning in Cirvix AgentControl: the gateway pins what was approved and holds anything new for approval instead of failing silently. How often do your servers bump in practice? Weekly, or with every deploy?

Collapse
 
autenai profile image
Auten •

Good list. One class of server breaks the "narrow the tools" advice by design: computer-use / screen-control servers. A click plus type pair is the user's whole desktop, so there is no path or glob to scope it to. The terminal, the password manager and the browser with saved sessions are all one click away.

Two things from your checklist change shape for that class:

  • Secrets. Stripping the env is not enough, because the risk is the agent typing a password it can see, or reading one off the screen. What works better is a saved-login tool: the server types the credential itself and the value never enters the model's context. Pin each credential to the site or app where it was first used, so a prompt-injected page on another origin gets nothing typed.
  • Audit. Tool params alone don't tell you what happened ("click 412,388"). Log the target element's label and the window title next to the coordinates, otherwise the log can't be replayed by a human.

Disclosure: I work on Auten, a computer-use MCP server, so these are the problems we hit, not theory.

Collapse
 
umangcirvix profile image
Umang Kumar •

This is the best counterexample to my list. Computer-use servers have no path to scope, so the boundary has to move to the credential and the target. Pinning each credential to its origin and never letting the value into context is smart. Logging the element label plus window title is going straight into my notes for the audit-log design. Do you hold actions per window, e.g. any click inside the password manager waits for a human?

Collapse
 
autenai profile image
Auten •

Not as an enforced per-window rule, no. Today the hold sits one layer up: our Claude Code plugin ships a safety skill that tells the agent to ask the user before buying, deleting, sending, granting a permission or entering a password, and the server has an ask_user tool for exactly that pause. That is guidance the model follows, not a gate it can't skip.

For your audit design I'd put the hold below the model: a deny-or-confirm list matched on app/window identity before the action runs. A prompt-injected page can talk a model out of a rule in its context; it can't talk it out of a check it never sees. The window title is weak for that (pages control it), the app's bundle id or process path is the stronger key.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.