Implementing Content Security Policies for MCP Resources

Kashish Hora

Kashish Hora

Co-founder of MCPcat

Try out MCPcat

The Quick Answer

Implement CSP headers in your MCP server to block XSS attacks and unauthorized resource loading:

// Express.js middleware for MCP server
app.use((req, res, next) => {
  res.setHeader(
    'Content-Security-Policy',
    "default-src 'self'; " +
    "script-src 'self' 'nonce-{RANDOM}'; " +
    "connect-src 'self' ws://localhost:* wss://*.mcpserver.com; " +
    "frame-ancestors 'none'"
  );
  next();
});

This policy restricts scripts to same-origin and nonce-validated sources, allows WebSocket connections for MCP transport, and prevents clickjacking. Replace {RANDOM} with a cryptographically secure random value per request.

Prerequisites

  • Node.js 18+ with Express.js or similar web framework
  • MCP server with web interface or API endpoints
  • Basic understanding of HTTP security headers
  • SSL/TLS certificate for production deployments (wss:// connections)

Installation

Install Helmet.js for comprehensive security headers including CSP:

$npm install helmet

For development environments, install CSP reporting tools:

$npm install express-csp-header uuid

Configuration

Content Security Policy headers control which resources browsers can load when accessing your MCP server. Since MCP servers often handle sensitive context data and tool executions, proper CSP configuration is critical for preventing injection attacks.

Basic CSP Configuration with Helmet.js

const helmet = require('helmet');
const crypto = require('crypto');

app.use((req, res, next) => {
  // Generate nonce for this request
  res.locals.nonce = crypto.randomBytes(16).toString('base64');
  next();
});

app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'", (req, res) => `'nonce-${res.locals.nonce}'`],
    styleSrc: ["'self'", "'unsafe-inline'"], // Consider using nonces for styles too
    imgSrc: ["'self'", "data:", "https:"],
    connectSrc: ["'self'", "ws://localhost:*", "wss://*.mcpserver.com"],
    fontSrc: ["'self'"],
    objectSrc: ["'none'"],
    mediaSrc: ["'none'"],
    frameAncestors: ["'none'"],
    baseUri: ["'self'"],
    formAction: ["'self'"],
    upgradeInsecureRequests: []
  }
}));

The connectSrc directive is particularly important for MCP servers as it controls WebSocket connections used for client-server communication. The frameAncestors directive prevents your MCP interface from being embedded in iframes, protecting against clickjacking attacks.

Report-Only Mode for Testing

Before enforcing CSP in production, use report-only mode to identify policy violations without breaking functionality:

app.use(helmet.contentSecurityPolicy({
  directives: {
    // ... same directives as above
  },
  reportOnly: true,
  reportUri: '/csp-violation-report'
}));

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

Usage

Implementing Nonce-Based CSP for Dynamic Content

MCP servers often generate dynamic UI elements or execute client-side scripts for tool interactions. Use nonces to allow these while maintaining security:

// Template rendering with nonce
app.get('/dashboard', (req, res) => {
  const nonce = res.locals.nonce;
  res.render('dashboard', { 
    nonce,
    cspNonce: `nonce="${nonce}"`
  });
});
<!-- In your template -->
<script nonce="<%= nonce %>">
  // This script will execute because it has the correct nonce
  const mcpClient = new MCPClient({
    endpoint: 'wss://localhost:3000'
  });
</script>

Securing WebSocket Connections

MCP uses WebSocket connections for real-time client-server communication. Configure CSP to allow only trusted WebSocket endpoints:

const cspDirectives = {
  connectSrc: [
    "'self'",
    process.env.NODE_ENV === 'development' ? "ws://localhost:*" : null,
    "wss://api.mcpserver.com",
    "wss://tools.mcpserver.com"
  ].filter(Boolean)
};

This configuration allows WebSocket connections to localhost in development while restricting production to specific secure endpoints.

Handling External Tool Resources

When MCP servers integrate with external tools, you may need to allow specific external resources:

// For MCP servers using external APIs or CDNs
const toolSpecificCSP = {
  scriptSrc: ["'self'", "https://cdn.playwright.dev"], // For browser automation tools
  imgSrc: ["'self'", "https://github.com", "https://avatars.githubusercontent.com"], // For GitHub integration
  connectSrc: ["'self'", "https://api.github.com", "wss://mcp-relay.example.com"]
};

Common Issues

Error: "Refused to execute inline script"

Inline scripts are blocked by CSP unless explicitly allowed. This commonly affects onclick handlers and script tags with inline code.

Root cause: CSP's script-src directive blocks all inline JavaScript by default to prevent XSS attacks. Even legitimate inline scripts from your own code are blocked without proper authorization.

// Fix: Move inline scripts to external files
// Before (blocked):
<button onclick="executeTool()">Run Tool</button>

// After (allowed):
<button id="toolButton">Run Tool</button>
<script src="/js/tools.js" nonce="<%= nonce %>"></script>

Prevention: Design your MCP interface to avoid inline scripts from the start. Use event listeners in external JavaScript files and data attributes for passing configuration.

Error: "Refused to connect to 'ws://localhost:3000'"

WebSocket connections require explicit CSP permission, even to same-origin endpoints.

Root cause: The CSP connect-src directive doesn't automatically include WebSocket protocols (ws:// and wss://) when you specify 'self'. This is because WebSockets use a different protocol than HTTP/HTTPS.

// Fix: Explicitly allow WebSocket protocols
app.use(helmet.contentSecurityPolicy({
  directives: {
    connectSrc: [
      "'self'",
      "ws://localhost:3000",     // Development
      "wss://mcp.example.com"    // Production
    ]
  }
}));

Prevention: Always include both ws:// and wss:// protocols in your connect-src directive when building MCP servers that use WebSocket transport.

Error: "Multiple CSP headers detected"

Conflicting CSP headers can cause unexpected behavior and policy enforcement issues.

Root cause: CSP headers may be set at multiple levels - by your application, web framework, reverse proxy, or CDN. When multiple policies exist, content must satisfy all of them, making the effective policy more restrictive than intended.

// Fix: Check and remove duplicate CSP headers
app.use((req, res, next) => {
  // Remove any existing CSP headers
  res.removeHeader('Content-Security-Policy');
  res.removeHeader('X-Content-Security-Policy'); // Legacy header
  
  // Set your CSP
  res.setHeader('Content-Security-Policy', 'your-policy-here');
  next();
});

Prevention: Document where CSP headers are set in your infrastructure and use a single source of truth for policy configuration.

Examples

Basic MCP Resource Server with Strict CSP

This example shows a minimal MCP server that serves tool definitions and resources with a strict CSP policy:

const express = require('express');
const helmet = require('helmet');
const crypto = require('crypto');

const app = express();

// Generate nonce middleware
app.use((req, res, next) => {
  res.locals.nonce = crypto.randomBytes(16).toString('base64');
  next();
});

// Strict CSP for MCP resource server
app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'none'"],
    scriptSrc: ["'self'", (req, res) => `'nonce-${res.locals.nonce}'`],
    styleSrc: ["'self'", (req, res) => `'nonce-${res.locals.nonce}'`],
    imgSrc: ["'self'"],
    connectSrc: ["'self'"],
    fontSrc: ["'self'"],
    manifestSrc: ["'self'"],
    frameAncestors: ["'none'"],
    baseUri: ["'none'"],
    formAction: ["'none'"]
  }
}));

// MCP tool endpoint
app.get('/tools', (req, res) => {
  res.json({
    tools: [{
      name: 'file_reader',
      description: 'Read file contents',
      parameters: { path: { type: 'string' } }
    }]
  });
});

app.listen(3000);

This configuration provides maximum security by defaulting to 'none' and explicitly allowing only necessary resources. The nonce-based approach ensures that only server-generated scripts can execute, preventing injection of malicious code even if an attacker finds an XSS vulnerability.

WebSocket-Enabled MCP Gateway with CSP

For MCP servers that handle real-time bidirectional communication, WebSocket support is essential:

const express = require('express');
const { WebSocketServer } = require('ws');
const helmet = require('helmet');

const app = express();
const server = require('http').createServer(app);

// CSP configuration for WebSocket MCP server
app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'", "'sha256-" + generateHashForInlineScript() + "'"],
    styleSrc: ["'self'", "https://fonts.googleapis.com"],
    fontSrc: ["'self'", "https://fonts.gstatic.com"],
    imgSrc: ["'self'", "data:", "https:"],
    connectSrc: [
      "'self'",
      "ws://localhost:3000",
      "wss://mcp-gateway.example.com",
      "https://api.anthropic.com" // For model queries
    ],
    workerSrc: ["'self'", "blob:"], // For web workers
    frameAncestors: ["'none'"],
    upgradeInsecureRequests: process.env.NODE_ENV === 'production' ? [] : null
  }
}));

// WebSocket server for MCP protocol
const wss = new WebSocketServer({ server });

wss.on('connection', (ws) => {
  ws.on('message', (data) => {
    const message = JSON.parse(data);
    // Handle MCP protocol messages
    if (message.method === 'tools/list') {
      ws.send(JSON.stringify({
        id: message.id,
        result: { tools: getAvailableTools() }
      }));
    }
  });
});

function generateHashForInlineScript() {
  // Calculate SHA256 hash of your inline script
  const script = 'console.log("MCP Gateway Initialized");';
  return require('crypto').createHash('sha256').update(script).digest('base64');
}

server.listen(3000);

Production deployments should enforce HTTPS and use wss:// for all WebSocket connections. The hash-based CSP approach shown here works well for static inline scripts that don't change frequently, while the connect-src directive ensures WebSocket connections are restricted to trusted endpoints only.