Drawing the right boundaries for an MCP server
An MCP server is a security boundary, not a convenience layer. Six rules that keep tool surfaces narrow enough to defend without making them too narrow to use.
An MCP server is a security boundary, not a convenience layer. Six rules that keep tool surfaces narrow enough to defend without making them too narrow to use.
The Model Context Protocol gives agents a clean way to reach into the rest of your stack — databases, dashboards, ticket systems, file stores. The protocol itself is small and well-specified. The interesting work is upstream of it: deciding what an MCP server is allowed to do, what shape its tools take, and where the boundary sits between the agent and your production systems.
Treat each MCP server as a security boundary, not a convenience layer. The rules below come from running production agents against real internal systems and watching where the failure modes show up.
An MCP server bundles a set of tools that share an auth context. Don't mix concerns. A "homelab tools" server that exposes infrastructure controls, secret reads, and media-library queries is three servers pretending to be one. When something goes wrong, you can't revoke just the part that misbehaved. Split servers along blast-radius lines: infra, observability, data, library — each with its own credentials and its own scope of what it's allowed to touch.
The cheapest control is the one you don't have to revoke. A server with only read tools is unsurprising. The moment you introduce a write tool, the server's threat model changes — every read tool is now a potential reconnaissance step before a write. Mark write tools clearly in their names (create_, delete_, update_) so the agent's planner can see them and so policy gates can intercept them.
prom_query beats http_get. list_open_tickets beats db_select. The name is a contract with the model — it's how the planner reasons about which tool to reach for. A generic transport-shaped tool is a foot-cannon: it forces the model to invent the right query, and gives you nowhere to put a guardrail. Wrap the transport in a domain-shaped tool and put the guardrail inside the wrapper.
Every tool result lands in the model's context window. An unbounded tool — one that can return a thousand log lines, or a database row at any size — turns a single tool call into a context-budget exhaustion event. Paginate by default. Truncate large fields with an explicit marker. Document the maximums in the tool description so the model knows when to ask for more rather than re-running the same query and hoping for less.
A failed tool call is not a stack trace. Return a structured error with a code, a short human description, and — when relevant — a suggested next action. Raw exception text is at best noise and at worst a leak of internal paths, hostnames, or schemas. The model handles structured errors gracefully; it spirals on unstructured ones.
MCP isn't a permissions system. It's a transport. The server is where you decide which tools exist for this caller, what the caller's identity is, and what credentials each tool will use downstream. Don't pass agent-supplied tokens into tool implementations. Don't read auth headers inside the tool. Establish the trust context once, at the server boundary, and let the tools execute against an already-resolved identity.
agent │ │ stdio / sse ▼ MCP server (one trust boundary) │ ├── auth resolution — shared secret · workload identity · OBO │ ├── tool: prom_query read · bounded ├── tool: prom_alerts read · bounded └── tool: silence_alert write · policy-gated │ ▼ downstream system (narrow API wrapper, never raw SDK)
The combination is what matters. A server with the right boundary, the right tool shapes, the right output bounds, and the right auth model is something you can hand to an agent without losing sleep. Get any of the six wrong and the failure mode shows up in production, where it's expensive to fix.
The protocol itself is the easy part. The discipline of where to draw the line — that's the work.