Your MCP Server Will Break Production : Here’s How to Stop It
The design principles for building MCP Servers for Infrastructure Management
In the last 48hrs i have build 2 production grade MCP servers including their Skills. On my whiteboard i build and erased the architecture 2 times, the 3rd attempted worked for me. So i thought to document my learnings for others who might find themselves on this route.
Every week, a new tutorial shows you how to build an MCP server in 10 minutes. Wire up some tools, point them at an API, ship it. The demos are impressive. The architecture is terrifying.
There are 2 types of MCP users
- Who add MCP servers in their favourite AI tools and using them independently across apps where they have regular user accounts (Gmail, Slack, Github etc)
- Who add MCP servers in their favourite AI tools and using them independently across apps where they have admin privileges and this app is is a critical provides critical infrastructure or services across the company. (AWS, Azure, Network Appliance , Server etc)
The moment your MCP server talks to a production system (where you have admin access), you’ve handed an AI agent a loaded weapon with no safety. That agent doesn’t understand blast radius. It doesn’t know that the delete call it just made removed firewall rules protecting 10,000 users. It executed the code, got a 200 OK, and moved on.
I built an MCP server that lets AI agents execute live API calls against FortiManager which is Fortinet’s centralized management platform for enterprise firewalls. Production firewalls. The kind that, if misconfigured, take down office networks, expose critical infrastructure, or silently open holes in perimeter security.
This post isn’t about FortiManager specifically. It’s about the architectural patterns that prevent MCP servers from becoming production incidents. If your MCP server talks to anything that matters like databases, cloud infrastructure, network devices, deployment pipelines these patterns apply to you.
Why Off-the-Shelf MCP Servers Won’t Save You
Most open-source MCP servers assume one of two things: the operations are low-stakes (reading files, querying docs), or the user is a developer who knows what they’re doing. Neither assumption holds when an AI agent is the user and production infrastructure is the target.
Fork a generic MCP server, point it at your FortiManager, and what do you get? An AI agent with unrestricted add, set, delete, and exec access to every firewall policy, address object, and device configuration in your network. No confirmation dialogs. No audit trail. No guardrails.
The MCP specification gives you a protocol. It defines how tools are registered, how inputs are validated, how results flow back. What it doesn’t give you is safety. That’s your architecture problem.
Think of it this way: MCP is the road. You still need seatbelts, speed limits, and crash barriers. The protocol is infrastructure agnostic by design, it has no concept of “this operation is catastrophically destructive.” That context lives in your domain, and your server needs to encode it.
The Architecture: Defense in Depth
No single safety mechanism is enough. Sandboxes have escapes. Configuration has bugs. Humans click “confirm” without reading. The only reliable approach is defense in depth, multiple independent layers, each catching what the previous one missed.
Here are the five patterns, with concrete examples from the FortiManager MCP server (applies to any high stake MCP server)
Pattern 1: Sandbox Everything
The AI agent sends JavaScript code to your MCP server. If you execute that code directly in your Node.js process, you’ve given it access to your filesystem, network stack, environment variables, and every secret in memory. Even if the tool description or prompt says “only call the FortiManager API,” the agent’s code can do whatever your process can do.
The fix is physical isolation. Not “please don’t access the filesystem” in prompt or tool description.
The FortiManager MCP server runs all agent-submitted code inside a QuickJS WASM sandbox. QuickJS is a JavaScript engine compiled to WebAssembly, running inside your Node.js process but completely isolated from it. The sandbox has no fs, no net, no process, no require. The only capabilities it gets are the ones explicitly injected.
Resource limits enforce hard boundaries: 64 MB memory cap, 30-second execution timeout, 512 KB stack, 50 API calls per invocation, and 1,000 log entries. If agent code tries to allocate a 1 GB array, exhaust the stack with infinite recursion, or hammer the API in a tight loop , the sandbox kills it. The host process is unaffected.
The key insight: the sandbox doesn’t just constrain what code should do. It constrains what code can do. That’s a fundamental difference. Policy is advisory; capability isolation is enforced.
Pattern 2: Default-Deny Method Gating
Sandboxing controls where code runs. Method gating controls what it can do once it’s running.
The FortiManager JSON-RPC API has nine methods: get, set, add, update, delete, exec, clone, move, replace. Only get is read-only. The other eight can modify production state. A single delete call to the wrong URL can remove an entire ADOM of firewall policies.
The method gate is simple and brutal: default-deny. Out of the box, only get is allowed. Every write method requires explicit opt-in via an environment variable.
Setting FMG_ALLOWED_WRITE_METHODS=set,add means only those two methods pass the gate. Not delete. Not exec. Not replace. The gate validates the actual method string in the API call at runtime , not the tool description, not the agent's stated intent, not a regex on the code. The enforcement happens inside the sandbox proxy, at the exact moment the call would be made.
This is a whitelist, not a blacklist. You don’t list what’s dangerous and hope you didn’t miss anything. You list what’s permitted and block everything else. When Fortinet adds a new API method next quarter, it’s blocked by default. No code change needed.
Pattern 3: Human-in-the-Loop via Elicitation
Method gating is machine enforcement. But some decisions should involve a human. When the agent wants to push a firewall policy change to 200 devices, someone should see that and say “yes, I meant to do that.”
The MCP specification includes an elicitation API, a mechanism for the server to ask the client (the human user) for input mid-operation. The FortiManager MCP server uses this to create a confirmation gate before any mutation executes.
When the server detects mutation method strings in the submitted code, it triggers an elicitation dialog:
“This code may execute mutation operations: set, add. FortiManager mutations can create, modify, or delete firewall policies, addresses, and device configurations. Do you want to proceed?”
The user sees a boolean confirmation. If they decline, cancel, or accept without confirming , execution is blocked. The agent gets a clear message: “Execution cancelled, user declined the mutation confirmation.”
The critical design choice: graceful degradation. Not every MCP client supports elicitation. When it’s unavailable, the server doesn’t fail open. It falls through to the method gate, which enforces the same allowlist regardless of whether a human confirmed. Elicitation is an additional layer, not a required one.
This means the system is safe even with the simplest MCP client. Better clients get a better experience, but safety never depends on client capabilities.
Pattern 4: Annotate Your Tools
The previous patterns are server-side enforcement. Tool annotations are client-side signals , metadata that tells the MCP client what kind of operation a tool performs.
The MCP spec defines four annotation hints: readOnlyHint, destructiveHint, idempotentHint, and openWorldHint. These don't enforce anything on the server, but they let intelligent clients make better decisions. A client might show a warning icon next to destructive tools, require extra confirmation, or deprioritize them in auto-planning workflows.
The FortiManager MCP server sets annotations dynamically based on mode. In read-only mode, the execute tool is annotated as readOnlyHint: true, destructiveHint: false. When write methods are enabled, it flips to readOnlyHint: false, destructiveHint: true.
The search tool, which queries the API specification, never the live system , is always readOnlyHint: true, destructiveHint: false, idempotentHint: true.
This is low-effort, high-value. A few lines of metadata, and every MCP client that understands annotations can surface appropriate warnings to users before the agent even starts composing a call.
Pattern 5: Fail-Fast Config Validation and Startup Audit Trail
The patterns above handle runtime. This one handles deployment.
Every environment variable is validated at startup through a Zod schema, not on first request, not lazily, not with silent defaults. Invalid write method names, malformed URLs, missing API tokens, all caught before the server accepts its first connection.
When the server starts, it writes its permission mode to logs:
[INFO] Mode: READ-ONLY — only "get" method allowedOr, when writes are enabled:
[WARN] Mode: WRITE ENABLED — allowed methods: get, set, addThat [WARN] is deliberate. Write mode is a conscious decision that should be visible in every log aggregator, every deployment dashboard, every ops channel that monitors this service. If someone enabled writes and didn't mean to, the evidence is in the first line of output.
The HTTP transport adds rate limiting (60 requests/minute per client IP) and a health endpoint with request statistics. These are operational basics, but they matter: a compromised or misbehaving client can’t DoS your FortiManager by flooding the MCP server with requests.
Putting It All Together
Here’s what a single request looks like as it passes through all five layers :
Five layers. Five independent checks. If any one fails, the request is blocked. No single layer is the hero , the stack is the hero.
The sandbox prevents arbitrary code execution. The method gate prevents unauthorized API methods. Elicitation prevents unintended mutations. Annotations prevent silent destructive calls. Config validation prevents misconfigured deployments.
Remove any one layer, and you’re probably fine most of the time. But “most of the time” isn’t a production safety guarantee. It’s a hope. And hope is not an architecture.
Closing Thoughts
MCP servers are genuinely powerful. Giving AI agents structured access to production systems enables workflows that were impossible a year ago, automated network auditing, intelligent configuration management, conversational infrastructure queries.
But power without safety architecture is negligence. If your MCP server can modify production state, architect it like you’d architect a deployment pipeline: defense in depth, least privilege by default, human checkpoints for irreversible actions, and audit trails that make the current state obvious.
The patterns are straightforward. The discipline to implement all of them, and resist the temptation to skip “just one layer” is the hard part.
Your MCP server doesn’t need to be the next production incident. It just needs to be built like production matters.
