To secure MCP servers you self-host, treat each server as a privileged, internet-adjacent service: pin the transport, authenticate every caller, scope tool permissions to least privilege, isolate the process, and force all outbound traffic through a default-deny egress proxy that inspects tool arguments and responses. Process hardening alone leaves the exfiltration path open.
What you are actually securing when you self-host MCP
A Model Context Protocol server is not a passive API. It is a bundle of executable capabilities (read a file, query a database, send an email, call an internal service) exposed to a non-deterministic client that decides, at runtime, which capability to invoke and with what arguments. When you self-host that server, you own four distinct attack surfaces:
- The transport: stdio pipes, Server-Sent Events, or Streamable HTTP, each with different exposure and different failure modes.
- The authorization boundary: who may connect, which tools they may list, and whose credentials the server uses downstream.
- The tool implementations: shell wrappers, SQL builders, HTTP fetchers, and file readers that accept model-generated input.
- The egress path: every outbound connection the server makes on behalf of a tool call, which is where stolen data actually leaves.
Most published guidance covers the first three and stops. That is why a hardened MCP server can still be the vehicle for a clean, quiet data exfiltration: the tools were legitimate, the auth was valid, and the destination host was simply never checked.
A working threat model for self hosted MCP security
Build your controls against specific adversary moves, not vague risk categories.
| Threat | How it lands | Control layer that stops it |
|---|---|---|
| Unauthenticated tool invocation | HTTP transport bound to 0.0.0.0 with no auth middleware | Network binding plus mandatory bearer or mTLS auth |
| Tool poisoning via description injection | Malicious instructions embedded in a tool description or schema | Description pinning and hashing, runtime argument inspection |
| Confused deputy on downstream APIs | Server holds broad credentials, model picks the target | Per-tool scoped credentials, per-action authorization |
| Command or SQL injection through arguments | Model-generated string concatenated into a shell or query | Strict JSON Schema validation, parameterized execution |
| Data exfiltration through a legitimate tool | Fetch or webhook tool called with an attacker-controlled URL | Default-deny egress allowlist with payload inspection |
| Dependency and supply chain drift | Unpinned package pulled at container build or runtime | Lockfiles, registry allowlist enforced at egress |
MCP server security best practices: a hardening sequence
Work through these in order. Each step assumes the previous one is done.
- Choose the narrowest transport that works. If the client and server share a host or container, use stdio and never open a port. If you need remote access, use Streamable HTTP behind a reverse proxy, bind the server to
127.0.0.1, and let the proxy own TLS and auth. Do not expose SSE endpoints directly to a VPC-wide CIDR. - Authenticate every connection. Require OAuth 2.1 with resource indicators or mTLS. Reject anonymous
initializecalls. Never forward the client token verbatim to downstream APIs: mint a separate, narrowly scoped credential server-side. We cover the pitfalls in depth in how to secure MCP server authentication and OAuth. - Scope tool exposure per caller. The
tools/listresponse should be a function of the authenticated identity, not a static array. A read-only analytics agent should never see the tool that issues refunds. - Validate arguments with strict schemas. Set
additionalPropertiesto false, bound string lengths, constrain enums, and reject anything that does not parse. Treat every argument as adversarial input authored by an attacker who controls the model context. - Never build shell or SQL strings from model output. Use parameterized queries and argument arrays for subprocess execution. If a tool genuinely needs a shell, put an allowlist of exact binaries in front of it and drop the shell interpreter entirely.
- Isolate the process. Run each MCP server as a non-root user in its own container with a read-only root filesystem, no privileged capabilities, a seccomp profile, and a tmpfs for scratch. Mount only the directories a file tool actually needs, and mount them read-only where possible.
- Pin tool definitions. Hash each tool name, description, and input schema at approval time and compare on every server start and every capability refresh. A silent change is a rug pull, not a release. See MCP rug-pulls and tool description drift for the detection pattern.
- Cap resource consumption. Per-tool timeouts, per-caller concurrency limits, and per-minute invocation ceilings prevent a looping agent from turning your MCP server into an amplifier against internal services.
- Log the full call, not the summary. Record caller identity, tool name, complete arguments, response size, downstream hosts contacted, and the policy decision. Ship it out of the server host so a compromised container cannot rewrite its own history.
- Put a default-deny egress proxy in front of everything. This is the step that survives the failure of steps one through nine.
Harden MCP server egress: the control that assumes breach
Assume an attacker got through. Maybe a prompt injection in a retrieved document persuaded the agent to call your fetch_url tool with an attacker domain. Maybe a transitive npm dependency in your server added a beacon. Maybe a tool description drifted and the model now believes it must forward configuration to a debugging endpoint. In every case, the compromise only becomes a breach when bytes leave your network.
Egress enforcement is the one layer that does not depend on the model behaving, the tool being correctly implemented, or the dependency tree being clean. Configure it as follows:
- Default deny, by identity. Every MCP server gets its own egress identity and its own allowlist. The Postgres MCP server should reach exactly one database endpoint and nothing else. The GitHub MCP server reaches
api.github.comand nothing else. Wildcards on public suffixes are not an allowlist. - Block the metadata endpoint.
169.254.169.254, link-local ranges, and RFC 1918 targets should be denied unless a specific rule permits a specific internal host. A single SSRF-shaped tool call is otherwise enough to lift cloud role credentials. - Inspect arguments and responses, not just hostnames. Hostname allowlisting stops the obvious exfil domain and misses the attacker who posts your customer table to an approved SaaS webhook. Deep inspection of the JSON-RPC payload is the difference, and it is the gap most gateways leave open. See deep tool-argument inspection.
- Normalize before matching. Base64, URL encoding, gzip, and homoglyph substitution all defeat naive regex DLP. Decode and normalize, then match secret and PII patterns.
- Gate irreversible actions on human approval. Deletes, transfers, production writes, and anything touching a customer-visible system should pause at the proxy and wait for an explicit approve or deny, with the full request body shown to the approver.
- Constrain the package registry. If your server installs dependencies at build or runtime, allow only your internal mirror. This kills hallucinated-package installs and typosquat pulls at the network layer.
Agent G runs as a drop-in proxy on that path. Point the MCP server host at it with standard proxy environment variables or a sidecar route, and every outbound connection (HTTP, WebSocket, DNS, and TLS-inspected JSON-RPC) is evaluated against versioned policy before it completes. Policy decisions and full request bodies land in an out-of-band log that a compromised server cannot edit. The MCP gateway capability handles the protocol-aware half: parsing tools/call frames, matching on tool name and argument values, and blocking or escalating per rule.
Validating that your hardening actually holds
Hardening you have not tested is documentation. Run these probes against a staging instance:
- Call a tool with an argument containing a path traversal sequence and confirm schema validation rejects it before the handler runs.
- Point a fetch-style tool at
http://169.254.169.254/latest/meta-data/and confirm the proxy denies it and logs the attempt. - Stage a fake credential string in a file the server can read, then ask an agent to summarize that file to an external endpoint. Confirm the DLP pass blocks it even when base64 encoded.
- Modify a tool description out of band and confirm the pinned-hash check fires on restart.
- Enumerate outbound connections from the container over a full day and reconcile them against the allowlist. Unexpected hosts are either a missing rule or a finding. The technique in shadow MCP discovery from egress logs applies to your own servers too.
Frequently asked questions
Is stdio transport inherently more secure than HTTP for MCP?
Stdio removes the network listener, which eliminates unauthenticated remote access. It does not remove the risk: the server still executes model-chosen tool calls and still makes outbound connections. Stdio narrows the inbound surface only. Argument validation, credential scoping, and egress enforcement remain mandatory.
Do I still need egress control if my MCP server only reads local files?
Yes. A file-reading server becomes an exfiltration engine the moment its output flows to any tool with network reach, and its own dependency tree can open connections you never authorized. Default-deny egress converts a read-only server into a genuinely contained one.
How do I secure MCP servers built by other teams?
You cannot audit every implementation, so enforce externally. Require each server to run behind the shared egress proxy with its own identity and allowlist, mandate schema-strict tools, and pin tool hashes at onboarding. Policy at the network boundary applies uniformly regardless of who wrote the handler.
What logging do auditors expect for self-hosted MCP?
They expect caller identity, timestamp, tool invoked, full arguments, downstream destination, policy decision, and approver identity for any gated action. Logs must be written outside the server trust boundary so a compromised container cannot alter the record retroactively.
Bringing it together
You secure MCP servers in layers: a narrow transport, real authentication, per-identity tool scoping, schema-strict arguments, an isolated process, pinned tool definitions, and complete logging. Then you assume all of that fails and add the layer that still holds, a default-deny egress boundary that inspects what the server sends and where it sends it. Related reading: preventing MCP tool poisoning at runtime and the Agent G overview.
Ready to enforce this on your own MCP fleet? Request access to the Agent G private beta and put a default-deny, inspection-capable egress proxy in front of every self-hosted MCP server you run.