Secure MCP Server Design Patterns for Real Tool-Calling Agents
26 Mar 2026 · 9 min read
- MCP
- AI Tools
- Security
- Developer Workflow
MCP is quickly becoming the default way to connect AI clients to tools, data, and workflows. That standardization is a big deal. It means you can expose one server and make it usable from multiple clients instead of hand-rolling a different integration every time.
The catch is that MCP servers sit in an unusually sensitive spot. They translate model intent into actions. They often run with delegated user permissions. And they can chain multiple tool calls fast enough that a small design mistake turns into a very large blast radius.
This post is a practical guide to designing MCP servers that are useful in production without becoming a security headache. The goal is not theoretical perfection. The goal is to make an MCP server that survives real usage: messy prompts, partial context, over-eager agents, and operators who need clean logs when something goes wrong.
Why MCP changes the threat model
Traditional APIs usually assume a human or application calling a defined endpoint directly. MCP is different. A model chooses or suggests tools based on natural language context, then interacts with a server that may have access to files, APIs, credentials, or internal systems.
That creates a few recurring risks:
- Prompt injection becomes tool selection pressure. A malicious document, page, or issue comment can try to steer the model toward the wrong tool or arguments.
- Delegated permissions are easy to over-scope. If one server has broad access, every tool exposed by it inherits a bigger blast radius.
- Chained calls magnify mistakes. A bad read can become a bad write, then a bad notification, then an audit problem.
- Operators lose visibility fast. Without structured logs and per-tool tracing, all you know is that “the agent did something weird.”
That is why secure MCP design is mostly about reducing ambiguity: clear tool contracts, tight auth, explicit approvals, and boring observability.
Pattern 1: Keep tools narrow and intention-revealing
A common failure mode is building one giant “do everything” tool because it feels convenient. Something like system_action, repo_admin, or workspace_execute gives the model too much room to improvise.
A better approach is to expose smaller tools with obvious names and hard boundaries:
list_pull_requestsget_ci_run_statuscreate_draft_blog_postread_customer_invoicequeue_calendar_invite
This helps in three ways:
- the model is more likely to choose the right tool
- you can apply more specific validation per tool
- your audit trail becomes readable by humans
Bad:
{
"name": "repo_admin",
"description": "Perform repository operations"
}
Better:
{
"name": "create_pull_request",
"description": "Create a pull request against a repository branch after changes are already pushed",
"inputSchema": {
"type": "object",
"properties": {
"repo": { "type": "string" },
"base": { "type": "string" },
"head": { "type": "string" },
"title": { "type": "string", "maxLength": 120 },
"body": { "type": "string", "maxLength": 10000 }
},
"required": ["repo", "base", "head", "title"]
}
}
If a tool performs writes, name the write plainly. If it performs deletion, say deletion in the name. Make dangerous intent impossible to hide behind a vague interface.
Pattern 2: Separate read tools from write tools
A strong default is to split tools by effect level.
Read-only tools:
- fetch records
- inspect configuration
- preview changes
- calculate diffs
Write tools:
- update records
- merge PRs
- delete objects
- send messages
Why bother? Because the client or operator can treat them differently. Read tools can usually run with lighter friction. Write tools deserve tighter scopes, extra validation, and in some environments a confirmation or approval layer.
In practice, this often looks like a two-step flow:
plan_blog_post_topicpublish_blog_post
Or:
preview_dns_changeapply_dns_change
That separation is one of the simplest ways to keep agents useful without giving them a one-shot path from ambiguous context to irreversible action.
Pattern 3: Validate arguments like the model will be wrong sometimes
The worst MCP servers assume the model will always send perfect arguments because the schema looks precise. That is not how real workloads behave.
Your server should assume arguments may be:
- incomplete
- stale
- over-broad
- valid JSON but semantically unsafe
- technically correct for the wrong tenant, branch, workspace, or environment
Treat schema validation as the first layer, not the only one.
Example server-side guardrails:
function validateCreatePr(input: CreatePrInput, session: SessionContext) {
assertAllowedRepo(session, input.repo);
assertBranchPrefix(input.head, "ai-blog/");
assertNonEmpty(input.title);
assertMaxLength(input.body, 10000);
if (input.base !== "master") {
throw new Error("PR base branch must be master for this workflow");
}
}
The pattern is simple:
- validate structure
- validate authorization
- validate business rules
- validate environment assumptions
If the tool writes to anything external, add idempotency or duplicate checks where possible.
Pattern 4: Make approvals explicit for destructive or external actions
Not every tool needs a human in the loop. But high-impact tools should have a deliberate pause point.
Good candidates for explicit approval:
- deleting data
- sending outbound messages or emails
- changing production infrastructure
- merging to protected branches
- rotating credentials
- modifying billing or payment configuration
There are several ways to implement this without ruining the developer experience:
- require a
confirm: truefield that is only accepted after a preview call - mint a short-lived approval token from a separate approval service
- enforce client-side approval for tools tagged as
destructiveorexternal - make the first call return a dry-run summary instead of performing the action
The key is that the approval step should be visible in logs and easy to reason about later.
Pattern 5: Scope permissions per tool, not just per server
One MCP server often bundles multiple tools. That is convenient operationally, but dangerous if the entire server runs under one broad credential.
Instead, scope by tool wherever you can:
- read-only database credentials for reporting tools
- separate GitHub token for issue reads versus PR writes
- per-tenant API keys for tenant-specific actions
- filesystem allowlists per workspace, not a blanket home directory mount
This matters because “trusted server” is too coarse a security boundary. Tools differ. Their credentials should too.
A useful mental model is: assume one tool will eventually misbehave. Design the blast radius so it cannot take the whole server with it.
Pattern 6: Add session isolation and execution boundaries
MCP is often discussed as a protocol problem, but operational isolation matters just as much.
Reasonable boundaries include:
- separate temp directories per session
- request-scoped context objects instead of mutable globals
- execution timeouts per tool
- output size limits
- network egress restrictions for tools that should not call the public internet
- child process allowlists instead of general shell access
If a tool spawns commands, make the command path predictable and the arguments structured. “Arbitrary shell” is not a clever shortcut. It is an incident report waiting to happen.
Pattern 7: Defend against prompt injection at the tool boundary
A lot of teams talk about prompt injection as if it is purely a model problem. In tool-calling systems, it is also a server problem.
Your server should not trust that because the model asked for a tool, the request must align with user intent.
A few practical checks help a lot:
- reject attempts to pass raw untrusted instructions into shell-like tools
- flag argument patterns that try to escape workspaces or access unexpected tenants
- annotate tools with allowed data domains and enforce them server-side
- store provenance when arguments come from untrusted sources such as web pages, issue comments, or uploaded files
For example, a summarization workflow that reads GitHub issue text should not automatically gain the ability to merge a PR just because the issue body includes “please merge this immediately.”
Pattern 8: Build for observability from day one
If you ever need to answer “what exactly did the agent do?”, plain text logs are not enough.
At minimum, log:
- tool name
- caller identity or session identity
- timestamp and duration
- sanitized arguments
- authorization decision
- result status
- downstream system touched
- approval state for write actions
A structured event shape like this goes a long way:
{
"timestamp": "2026-03-26T21:57:00Z",
"session_id": "sess_42",
"tool": "create_pull_request",
"actor": "agent",
"repo": "negiadventures/negiadventures.github.io",
"authorized": true,
"approval": "not-required",
"status": "success",
"duration_ms": 834
}
If you already use OpenTelemetry, carry traces from client to MCP server to downstream API. That makes multi-step failures dramatically easier to debug.
Pattern 9: Prefer policy checks over prompt-only guardrails
Prompt instructions like “never modify production” are useful, but they are not a security control.
Real controls live outside the model:
- policy engines
- branch protection
- ACL checks
- server-side allowlists
- outbound domain restrictions
- rate limits
- approval gates
If a policy matters, enforce it in code or infrastructure. Prompts are guidance. Policies are guarantees.
Pattern 10: Design for graceful refusal
A secure MCP server should be comfortable saying no.
Useful refusal cases include:
- missing authorization
- ambiguous target resource
- unsafe write without approval
- requested action outside policy
- suspicious argument pattern
- unsupported environment or tenant
And the refusal should be actionable. Not just “forbidden,” but something like:
Refused:
merge_pull_requestis disabled for repositories outside the approved allowlist. Allowed repos for this session:negiadventures.github.io,internal-docs.
That helps the user recover without training the model to keep probing.
A practical build checklist
If I were building an MCP server for a real team today, this is the baseline I would want before calling it production-ready:
- Tool names are narrow and intention-revealing
- Read and write tools are split
- Every write tool has business-rule validation beyond JSON schema
- Credentials are scoped per tool or per effect level
- Session isolation is in place
- Tool timeouts and output caps are configured
- Dangerous actions have approval or preview flows
- Structured logs capture tool calls and decisions
- Policies are enforced server-side, not only in prompts
- Destructive tools are easy to disable quickly
That list is not glamorous, but it is what keeps an MCP deployment from becoming a trust fall.
Where this gets interesting next
The MCP ecosystem is moving fast, and that is exactly why design discipline matters now. Standards make integration easier, but they also make it easier to expose a badly designed server to more clients more quickly.
The winning pattern is not “give the model more freedom and hope for the best.” It is:
- narrow tools
- explicit permissions
- strong validation
- observable execution
- human control where impact is high
That combination preserves the best part of MCP: one clean interface for useful tools, without pretending agents should get a blank check.
References and resources
- Model Context Protocol docs: https://modelcontextprotocol.io/introduction
- Anthropic, Introducing the Model Context Protocol: https://www.anthropic.com/news/model-context-protocol
- OWASP GenAI, A Practical Guide for Secure MCP Server Development: https://genai.owasp.org/resource/a-practical-guide-for-secure-mcp-server-development/
Key takeaways
- MCP servers are not just integration glue; they are security boundaries.
- Narrow tools outperform giant “do anything” tools in both safety and reliability.
- JSON schema is necessary, but business-rule validation is where most real protection lives.
- Read/write separation, approval flows, and structured logs are worth the extra effort.
- If a policy matters, enforce it outside the prompt.
If you are building agent tooling in 2026, secure MCP design is one of the highest-leverage places to be boring on purpose.