Chapters — Connection Layer (MCP)
MCP·Running It Well·8 min read·Sep 5, 2026

Security and Trust Boundaries

Authenticating and authorizing tool calls, defending against prompt injection through tool input, and rate limiting a server against abuse.

Every tool call needs a trust decision

A tool that touches real data or takes real action is only as safe as the weakest check standing between "the model requested this" and "the server did it." Three checks matter most: who is making this call, are they allowed to do this specific thing, and is the input itself safe to act on.

Authentication and authorization

Continuing the support desk server from the last chapter, every sensitive call should resolve to a real, authorized user before it does anything:

class SupportDeskServer(FastMCP):
    def __init__(self):
        super().__init__("Support Desk")
        self.auth = AuthManager()
        self.permissions = PermissionChecker()

    async def _authenticate(self, context: Context) -> User:
        token = context.headers.get("Authorization", "").removeprefix("Bearer ")
        if not token:
            raise AuthenticationError("Missing credentials")
        user = await self.auth.validate_token(token)
        if not user:
            raise AuthenticationError("Invalid or expired token")
        return user

    @mcp.tool()
    async def escalate_ticket(self, context: Context, ticket_id: str, reason: str) -> dict:
        """Escalate a ticket to a senior agent"""
        user = await self._authenticate(context)

        if not await self.permissions.can(user, "escalate", ticket_id):
            raise AuthorizationError(f"{user.id} cannot escalate ticket {ticket_id}")

        await self.audit_log.record(user, "escalate", ticket_id, reason)
        return await self._perform_escalation(ticket_id, reason)

Authentication answers "who is this." Authorization answers "can they do this specific thing." Skipping straight to executing the operation after only checking that a token exists is a common shortcut, and it's the one that turns into an incident later.

Prompt injection through tool input

Text that flows into a tool, whether typed by a user or pulled from a resource, can contain something that reads like an instruction rather than data: "ignore previous instructions," "disregard the system prompt," and similar. A server should treat tool input as data, never as instructions to itself:

import re

INJECTION_PATTERNS = [
    r"ignore\s+(all\s+)?previous\s+instructions",
    r"disregard\s+(the\s+)?system\s+prompt",
    r"override\s+safety",
]

def sanitize_tool_input(text: str) -> str:
    """Strip patterns that look like an attempt to redirect the model's behavior"""
    for pattern in INJECTION_PATTERNS:
        text = re.sub(pattern, "", text, flags=re.IGNORECASE)
    return text

This isn't a complete defense on its own; pattern matching catches the obvious cases, not a determined attacker. The more durable protection is architectural: a tool should only ever be able to perform the specific action it was designed for, so even a successfully injected instruction has nowhere useful to go.

Rate limiting

A single misbehaving client, or a legitimate one in a retry loop, can overwhelm a server without any malicious intent at all:

from collections import defaultdict
from datetime import datetime

class RateLimiter:
    def __init__(self, calls_per_minute: int = 60):
        self.limit = calls_per_minute
        self.calls: dict[str, list[datetime]] = defaultdict(list)

    def allow(self, user_id: str) -> bool:
        now = datetime.utcnow()
        recent = [t for t in self.calls[user_id] if (now - t).seconds < 60]
        self.calls[user_id] = recent
        if len(recent) >= self.limit:
            return False
        recent.append(now)
        return True
A minimum bar worth holding to
Authenticate every call
Not just the ones that feel sensitive. Today's read-only tool is tomorrow's write path.
Authorize per action, not per user role alone
"Can view tickets" and "can escalate tickets" are different permissions.
Treat all tool input as untrusted
Sanitize it, and never let a tool's own description be the only safeguard against misuse.
Rate limit before you need it
Adding it after the first incident means the incident already happened.
Part of a free guide

Connection Layer (MCP)

A simple guide to MCP, the protocol that lets AI tools talk to the outside world.

Browse All Guides →