Implementing Content Security Policies for MCP Resources

Kashish Hora
Co-founder of 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.
Related Guides
Implementing CORS Policies for Web-Based MCP Servers
Configure Cross-Origin Resource Sharing (CORS) policies for web-based MCP servers to enable secure browser access.
Security tests for MCP server endpoints
Test MCP server security by validating authentication, authorization, and vulnerability scanning.
Error handling in custom MCP servers
Implement robust error handling in MCP servers with proper error codes, logging, and recovery strategies.