Guide · MCP Security

MCP server security headers

HTTP security headers are one-line defenses against entire categories of web attacks. Content-Security-Policy blocks XSS by restricting which scripts execute on your server's web UI. Strict-Transport-Security prevents HTTPS downgrade attacks. X-Frame-Options blocks clickjacking. These headers are cheap to add — a single middleware call or a few Caddy directives — and they significantly raise the cost of exploiting any XSS or injection bug your server might harbor. For MCP servers with web-facing endpoints or a status dashboard UI, they are a baseline requirement.

TL;DR

Add helmet() to your Express-based MCP server before any route handlers. Configure a Content-Security-Policy that restricts script sources to 'self' and explicitly listed CDNs. Set Strict-Transport-Security: max-age=31536000; includeSubDomains. Use X-Frame-Options: DENY unless you embed your UI in iframes. If your MCP server is behind Caddy (as on the factory VPS), add header directives in your Caddyfile instead — Caddy is your TLS terminator and injects headers before your Node process ever sees the request.

Security header reference

HeaderProtects againstRecommended value
Content-Security-Policy XSS, data injection, malicious script loading default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none'
Strict-Transport-Security HTTPS downgrade, SSL stripping max-age=31536000; includeSubDomains
X-Frame-Options Clickjacking DENY (or SAMEORIGIN if you embed your own UI)
X-Content-Type-Options MIME type sniffing nosniff
Referrer-Policy URL leakage in Referer header strict-origin-when-cross-origin
Permissions-Policy Browser API abuse (camera, geolocation, payment) camera=(), microphone=(), geolocation=(), payment=()
Cross-Origin-Resource-Policy Cross-origin data reads same-origin for API routes; cross-origin for public assets
Cross-Origin-Opener-Policy Cross-origin window access, Spectre side-channel same-origin

Helmet setup for Express-based MCP servers

Helmet is a collection of small Express middleware functions that set security headers. Install it with npm install helmet:

import express from 'express';
import helmet from 'helmet';
import cors from 'cors';

const app = express();

// helmet() MUST come before your route handlers and before cors()
// helmet sets conservative defaults; override CSP to match your app's needs
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc:  ["'self'"],             // No inline scripts, no external CDN
      styleSrc:   ["'self'", "'unsafe-inline'"],  // Allow inline styles (common in status pages)
      imgSrc:     ["'self'", 'data:'],   // data: for base64 inline images
      connectSrc: ["'self'"],            // AJAX/fetch only to your own origin
      fontSrc:    ["'self'"],
      objectSrc:  ["'none'"],            // Block plugins (Flash etc.)
      frameAncestors: ["'none'"],        // Prevents embedding in iframes (clickjacking)
      upgradeInsecureRequests: [],       // Rewrite http:// links to https://
    },
  },
  hsts: {
    maxAge: 31_536_000,          // 1 year in seconds
    includeSubDomains: true,
    preload: false,              // Set preload: true only once you're ready for HSTS preload list submission
  },
  frameguard: { action: 'deny' },
  noSniff: true,                 // X-Content-Type-Options: nosniff
  referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
  permittedCrossDomainPolicies: false,  // Block Adobe/Flash cross-domain
  crossOriginResourcePolicy: { policy: 'same-origin' },
  crossOriginOpenerPolicy: { policy: 'same-origin' },
}));

// CORS after helmet (CORS manages its own Allow-Origin headers)
app.use(cors(corsOptions));

Content-Security-Policy in depth

CSP is the most impactful and most complex security header. It instructs the browser on which sources are trusted for each type of resource. A strict CSP stops XSS from being exploitable even when an attacker can inject arbitrary HTML — injected scripts from external domains are blocked by the browser before they execute.

Common directives for MCP server status pages and web UIs:

// For an MCP server with a React/Vue frontend loading from a CDN:
contentSecurityPolicy: {
  directives: {
    defaultSrc:  ["'self'"],
    scriptSrc:   ["'self'", 'https://cdn.example.com'],  // Allow specific CDN
    styleSrc:    ["'self'", 'https://fonts.googleapis.com', "'unsafe-inline'"],
    fontSrc:     ["'self'", 'https://fonts.gstatic.com'],
    imgSrc:      ["'self'", 'data:', 'https://avatars.githubusercontent.com'],
    connectSrc:  ["'self'", 'https://api.yourserver.com'],
    workerSrc:   ["'none'"],
    frameAncestors: ["'none'"],
    reportUri:   ['/csp-report'],  // Collect CSP violations for debugging
  },
}

Start with a report-only policy (Content-Security-Policy-Report-Only) to collect violations without blocking anything, then tighten the policy once you understand what your app actually loads:

// Report-only mode: log violations but don't block (for gradual rollout)
app.use((req, res, next) => {
  res.setHeader(
    'Content-Security-Policy-Report-Only',
    "default-src 'self'; report-uri /csp-report"
  );
  next();
});

app.post('/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
  console.log('CSP violation:', JSON.stringify(req.body));
  res.status(204).end();
});

Configuring security headers in Caddy

If your MCP server is behind Caddy (the factory VPS setup), Caddy can set security headers at the reverse-proxy layer — before requests hit your Node process. This is cleaner for static sites and works even if your backend crashes:

alivemcp.com {
  # TLS is automatic via Let's Encrypt
  tls {
    on_demand
  }

  # Security headers — applied to all responses
  header {
    Strict-Transport-Security "max-age=31536000; includeSubDomains"
    X-Content-Type-Options    "nosniff"
    X-Frame-Options           "DENY"
    Referrer-Policy           "strict-origin-when-cross-origin"
    Permissions-Policy        "camera=(), microphone=(), geolocation=(), payment=()"
    Content-Security-Policy   "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none'"
    # Remove headers that leak server info
    -Server
    -X-Powered-By
  }

  # Static files
  root * /srv/sites/agent-12

  # Proxy API routes to Node backend
  handle /api/* {
    reverse_proxy localhost:3000
  }

  # Serve static files
  file_server
}

Caddy's header directive with a - prefix removes a header rather than setting it. -Server removes Caddy's own server identification header; -X-Powered-By removes Node/Express fingerprinting if your backend sets it.

HSTS and preloading

Strict-Transport-Security tells browsers to always use HTTPS for your domain for max-age seconds, even if the user types http://. The browser caches this instruction — subsequent visits never attempt HTTP at all, eliminating SSL-stripping attacks on cached visitors.

The HSTS preload list goes further: browsers ship with a baked-in list of domains that are always HTTPS. To be included:

Do not set preload: true until you're certain all your subdomains support HTTPS — the preload list is very hard to remove from once submitted.

Testing security headers

Use curl to inspect the headers your server actually sends:

curl -s -I https://alivemcp.com | grep -E "(Content-Security|Strict-Transport|X-Frame|X-Content|Referrer|Permissions)"

# Expected output:
# content-security-policy: default-src 'self'; script-src 'self'; ...
# strict-transport-security: max-age=31536000; includeSubDomains
# x-frame-options: DENY
# x-content-type-options: nosniff
# referrer-policy: strict-origin-when-cross-origin
# permissions-policy: camera=(), microphone=(), ...

For a comprehensive automated check, securityheaders.com grades your headers and flags missing or misconfigured ones. Run it after every deploy that touches your Caddyfile or middleware configuration.

Security headers and MCP server uptime

Security headers don't affect uptime directly, but they protect the web UI and status pages that users consult when your server is experiencing issues. An XSS vulnerability in your status page — compromised by a missing CSP — could display false status information or steal session tokens from users checking on an outage. AliveMCP monitors the transport-layer health of your MCP endpoints; pair that with solid security headers to protect the information layer users see.

Further reading