# Security Considerations for MCP Server Implementations: 10 Critical Hardening Strategies

> Learn essential MCP server security considerations. Discover 10 critical hardening strategies to protect your implementations from unauthorized access and data leaks.

- Repository: [Rohit Ghumare/ai-engineering-from-scratch](https://github.com/rohitg00/ai-engineering-from-scratch)
- Tags: best-practices
- Published: 2026-05-21

---

**Production-grade MCP servers require signed JWT verification, fine-grained scope enforcement with human-in-the-loop approval for destructive actions, strict input validation, PII redaction, and TLS encryption to prevent unauthorized tool invocation and data leakage.**

The reference scaffold in `rohitg00/ai-engineering-from-scratch` provides a minimal implementation of an MCP (Multi-Capability Protocol) server that demonstrates core dispatch logic, but production deployments require comprehensive security hardening. This guide analyzes the baseline implementation found in [`phases/19-capstone-projects/13-mcp-server-with-registry/code/main.py`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/phases/19-capstone-projects/13-mcp-server-with-registry/code/main.py) and provides specific hardening recommendations for authentication, authorization, and data protection.

## Authentication and Token Integrity

The scaffold uses simple dataclasses (`Token`) that carry a `user` identifier and scope set without cryptographic verification. According to the source code, the system trusts the passed-in token without verifying signatures, which allows token forgery in production environments.

**Recommended hardening:** Implement **signed JWTs** with RS256 or ES256 algorithms or use **mTLS-based mutual authentication**. Verify token signatures, expiration claims, and issuer identity before any policy check.

```python
from jwt import decode as jwt_decode, ExpiredSignatureError, InvalidTokenError

def verify_jwt(token_str: str, jwk: dict) -> Token:
    try:
        payload = jwt_decode(token_str, jwk, algorithms=["RS256"])
        return Token(
            user=payload["sub"],
            scopes=set(payload["scopes"]),
            approved_at=payload.get("approved_at", 0.0),
        )
    except (ExpiredSignatureError, InvalidTokenError) as exc:
        raise PermissionError(f"Invalid token: {exc}")

```

## Fine-Grained Scope Enforcement

The `policy_decide` function in [`main.py`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/main.py) (lines 85-94) checks that the token possesses required scopes via `Token.has_scope` before executing tools. However, the scaffold uses a simple string-based scope model that lacks granularity for complex authorization scenarios.

**Recommended hardening:** Adopt an **expressive scope model** (Resource-Action-Domain) stored centrally. Enforce **least-privilege by default** and require explicit approval scopes for privileged operations. Store scope definitions in a policy engine separate from application code.

## Human-in-the-Loop Approval for Destructive Actions

Destructive tools require explicit human authorization in the scaffold. The `policy_decide` logic (lines 92-98) validates the `approved:by:human` scope and checks `Token.fresh_approval` to ensure the approval timestamp is recent.

**Recommended hardening:** Integrate with formal **approval workflows** (Slack interactive cards, ServiceNow tickets, or GitHub issues). Store approvals in a **tamper-evident store** such as append-only logs or blockchain-backed audit trails. Enforce short validity windows (5-15 minutes) to prevent replay attacks.

## Input Validation and Payload Constraints

The scaffold caps JSON payload size to 8 KB (`len(json.dumps(args)) > 8192`) in lines 94-96, but lacks schema validation. Each `ToolSchema` includes an `input_schema` field, yet the dispatcher does not validate incoming arguments against it.

**Recommended hardening:** Run a **JSON-Schema validator** on arguments before policy checks. Apply **tool-specific size limits** rather than global caps, use streaming parsers for large uploads, and reject oversized bodies at the reverse proxy layer.

```python
from jsonschema import validate, ValidationError

def dispatch_with_validation(server, token, tool, args, audit):
    schema = server.tools[tool].input_schema
    try:
        validate(instance=args, schema=schema)
    except ValidationError as ve:
        audit.append(
            AuditEntry(time.time(), token.user, tool, f"denied:invalid_schema:{ve.message}",
                       redact(args), {})
        )
        return {"error": {"code": 400, "message": "Invalid arguments"}}
    return dispatch(server, token, tool, args, audit)

```

## Data Protection and Audit Logging

The `redact` function (lines 5-9) masks email addresses, SSNs, and phone numbers before persisting to the audit log via `AuditEntry`. This prevents PII leakage in logs but uses pattern matching rather than structured data classification.

**Recommended hardening:** Adopt a full-featured **data-loss-prevention library** such as Microsoft Presidio or Google Cloud DLP. Ensure audit logs are **write-once**, encrypted at rest, and stored in a tamper-evident system such as AWS QLDB or immutable S3 buckets.

## Registry Integrity and Trust

The in-memory `Registry` stores capability documents via `registry.register` without verifying server authenticity. The scaffold accepts any provided capability manifest, enabling man-in-the-middle attacks during tool discovery.

**Recommended hardening:** Use **signed capability manifests** (JWS) and a PKI to validate each server's `.well-known/mcp-capabilities` endpoint. Implement periodic key rotation and certificate pinning for high-security environments.

```python
import requests, base64, json
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import padding, hashes

def fetch_and_verify_capabilities(server_url: str, public_key_pem: bytes) -> dict:
    resp = requests.get(f"{server_url}/.well-known/mcp-capabilities")
    signed_doc = resp.json()
    payload = base64.urlsafe_b64decode(signed_doc["payload"])
    signature = base64.urlsafe_b64decode(signed_doc["signature"])
    public_key = serialization.load_pem_public_key(public_key_pem)
    public_key.verify(
        signature,
        payload,
        padding.PKCS1v15(),
        hashes.SHA256(),
    )
    return json.loads(payload)

```

## Transport Security

The scaffold runs as a pure Python script invoked via `python main.py` with no TLS configuration, exposing plaintext credentials and tokens on the network.

**Recommended hardening:** Deploy behind an **HTTPS reverse proxy** (nginx, Envoy, or AWS ALB) or embed TLS support directly using `aiohttp` or `uvicorn`. Enforce **HSTS headers**, secure cipher suites (TLS 1.3 only), and certificate transparency logging.

## Error Handling and Information Leakage

The `dispatch` block (lines 40-44) wraps errors but returns exception messages directly to the client, potentially exposing internal implementation details, file paths, or database connection strings.

**Recommended hardening:** Return **generic error codes** (e.g., `TOOL_EXECUTION_ERROR`) to clients while logging detailed diagnostics to a secure internal observability platform. Strip stack traces and sensitive variables from all external responses.

## Rate Limiting and DoS Mitigation

The scaffold contains no throttling mechanisms, making it vulnerable to resource exhaustion attacks via large payloads or rapid tool invocation.

**Recommended hardening:** Implement **per-client quotas** using Redis or in-memory stores with sliding window algorithms. Configure **reverse-proxy rate limiting** at the edge and apply exponential back-off on authentication failures to prevent credential stuffing.

## Summary

Securing an MCP server implementation requires defense in depth across the entire request lifecycle:

- **Verify** all tokens using cryptographic signatures before authorization checks
- **Enforce** fine-grained scopes and require fresh human approval for destructive operations
- **Validate** all inputs against JSON Schema and enforce strict payload size limits
- **Redact** PII using data-loss-prevention libraries before writing to audit logs
- **Protect** transport layers with TLS 1.3 and mutual authentication
- **Sign** capability manifests to ensure registry integrity
- **Sanitize** error messages to prevent information leakage

## Frequently Asked Questions

### What is the most critical security vulnerability in the basic MCP scaffold?

The lack of **token signature verification** is the most critical vulnerability. The scaffold trusts the `Token` dataclass without validating cryptographic signatures, allowing attackers to forge tokens and invoke arbitrary tools. Production implementations must verify JWTs using RS256 or implement mTLS before any `policy_decide` logic executes.

### How should destructive tool invocations be secured in an MCP server?

Destructive tools require **fresh human-in-the-loop approval** validated through the `Token.fresh_approval` timestamp check. Beyond the scaffold's simple timestamp validation, integrate with ticketing systems or chatOps workflows, store approvals in tamper-evident logs, and restrict approval validity to 5-15 minutes to prevent replay attacks.

### What pattern ensures MCP registry integrity against man-in-the-middle attacks?

Use **JWS-signed capability manifests** fetched from `.well-known/mcp-capabilities` endpoints. Verify these manifests against registered public keys in a PKI before trusting tool definitions. This ensures the registry contains authentic capability documents from legitimate servers, not injected malicious tools.

### How can PII leakage be prevented in MCP audit trails?

Implement the **`redact` function** pattern (lines 5-9) using structured data-loss-prevention libraries like Presidio rather than simple regex. Store redacted logs in write-once, immutable storage with encryption at rest. Ensure that raw arguments containing PII are never persisted to plaintext logs or returned in error messages.