Skip to main content
← Back to All Posts

Secure MCP Server Design Patterns for Real Tool-Calling Agents

March 26, 2026 • 10 min read
Secure MCP Server Design Patterns

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:

  1. 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.
  2. Delegated permissions are easy to over-scope. If one server has broad access, every tool exposed by it inherits a bigger blast radius.
  3. Chained calls magnify mistakes. A bad read can become a bad write, then a bad notification, then an audit problem.
  4. Operators lose visibility fast. Without structured logs and per-tool tracing, all you know is that “the agent did something weird.”

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_requests
  • get_ci_run_status
  • create_draft_blog_post
  • read_customer_invoice
  • queue_calendar_invite
{
  "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 should fetch, inspect, preview, and calculate. Write tools should update, merge, delete, and notify. That split makes it much easier to enforce different validation and approval rules.

In practice, that often means two-step flows such as:

  1. plan_blog_post_topic
  2. publish_blog_post

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. Real workloads are messier than that. Arguments can be incomplete, stale, over-broad, or valid JSON but still semantically unsafe.

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");
  }
}

Schema validation is just the first layer. The real protection comes from business-rule checks, authorization checks, and environment-specific assumptions.

Pattern 4: Make approvals explicit for destructive or external actions

Not every tool needs a human in the loop. But deleting data, sending outbound messages, changing production infrastructure, merging to protected branches, and rotating credentials should not feel casual.

Several approval patterns work well:

  • require a confirm: true field after a preview call
  • mint a short-lived approval token from a separate service
  • enforce client-side approvals for tools tagged destructive or external
  • make the first call return a dry-run summary instead of performing the action

Pattern 5: Scope permissions per tool, not just per server

One MCP server often bundles multiple tools. That is convenient, but dangerous if the whole server runs under one broad credential. Prefer separate GitHub tokens for issue reads versus PR writes, read-only database credentials for reporting tools, and workspace allowlists instead of a blanket filesystem mount.

Pattern 6: Add session isolation and execution boundaries

Operational isolation matters as much as protocol design. Reasonable boundaries include separate temp directories per session, request-scoped context instead of mutable globals, execution timeouts, output limits, and restricted network egress for tools that should not talk to the public internet.

Pattern 7: Defend against prompt injection at the tool boundary

Prompt injection is not just a model problem in tool-calling systems. It is also a server problem. A summarization workflow that reads GitHub issue text should not gain the ability to merge a PR just because the issue body says “please merge this immediately.”

Useful checks include rejecting raw untrusted instructions in shell-like tools, flagging attempts to escape workspaces or tenants, and storing provenance when arguments came from untrusted sources such as web pages or uploaded files.

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, actor or session identity, timestamp, duration, sanitized arguments, authorization decision, approval state, and result status.

{
  "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
}

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 in branch protection, ACL checks, allowlists, outbound restrictions, rate limits, and approval gates. If a policy matters, enforce it outside the prompt.

Pattern 10: Design for graceful refusal

A secure MCP server should be comfortable saying no. Refusals should be specific enough to help the user recover without training the model to keep probing. “Forbidden” is weak. “merge_pull_request is disabled for repositories outside the approved allowlist” is much better.

A practical build checklist

  • 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

References and resources

  • Model Context Protocol docs
  • Anthropic: Introducing the Model Context Protocol
  • OWASP GenAI: A Practical Guide for Secure MCP Server Development

Key takeaways

  1. MCP servers are not just integration glue; they are security boundaries.
  2. Narrow tools outperform giant “do anything” tools in both safety and reliability.
  3. JSON schema is necessary, but business-rule validation is where most real protection lives.
  4. Read/write separation, approval flows, and structured logs are worth the extra effort.
  5. If a policy matters, enforce it outside the prompt.
MCP AI Tools Security Developer Workflow

Browse more blog posts