Security Considerations for MCP Server Implementations: 10 Critical Hardening Strategies

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 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.

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 (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.

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.

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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →