they are communicating with matches the scanned artifact. This closes the gap between "code that was scanned" and "code that is running," enabling a zero-trust model for agent-tool interactions.
Core Solution
Securing an MCP server requires a layered defense strategy. We must implement rigorous static scanning during development, enforce runtime integrity checks, and establish a verifiable chain of custody for server updates.
1. Implementing Static Scanning with mcp-security-scan
Integrate mcp-security-scan into your CI/CD pipeline. The tool analyzes the server codebase for the five attack categories. Below is a TypeScript example demonstrating how to structure an MCP server with security patterns that pass static analysis, contrasted with vulnerable patterns.
Vulnerable Pattern: Unsafe Execution and Credential Leakage
// BAD: This pattern triggers alerts for unsafe exec and credential theft.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { exec } from "child_process";
const server = new McpServer({ name: "unsafe-server", version: "1.0.0" });
server.tool("run-command", "Execute a system command", {
cmd: z.string()
}, async ({ cmd }) => {
// VULNERABILITY: Direct execution of user input
exec(cmd, (err, stdout) => {
// VULNERABILITY: Returning raw output may leak sensitive data
return { content: [{ type: "text", text: stdout }] };
});
});
// VULNERABILITY: Exposing environment variables
server.tool("get-config", "Retrieve configuration", {}, async () => {
return {
content: [{ type: "text", text: JSON.stringify(process.env) }]
};
});
Secure Pattern: Whitelisting, Validation, and Secret Masking
// GOOD: This pattern adheres to security best practices.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { execFile } from "child_process";
import { promisify } from "util";
const execFileAsync = promisify(execFile);
const server = new McpServer({ name: "secure-server", version: "1.0.0" });
// Define allowed commands explicitly
const ALLOWED_COMMANDS = ["ls", "cat", "grep"] as const;
type AllowedCommand = typeof ALLOWED_COMMANDS[number];
server.tool("safe-exec", "Run a whitelisted command safely", {
command: z.enum(ALLOWED_COMMANDS),
args: z.array(z.string()).default([])
}, async ({ command, args }) => {
try {
// SAFE: Using execFile with argument array prevents injection
const { stdout } = await execFileAsync(command, args);
return { content: [{ type: "text", text: stdout }] };
} catch (error) {
return { content: [{ type: "text", text: "Execution failed" }] };
}
});
// SAFE: Returning only specific, non-sensitive config keys
server.tool("get-config", "Retrieve public configuration", {}, async () => {
const safeConfig = {
version: process.env.APP_VERSION,
region: process.env.AWS_REGION
};
return { content: [{ type: "text", text: JSON.stringify(safeConfig) }] };
});
2. Mitigating Filesystem and Exfiltration Risks
Static analysis flags unrestricted filesystem access and outbound network calls. The fix involves implementing strict path resolution and egress filtering.
import path from "path";
// SAFE: Path traversal protection
function resolveSafePath(userPath: string, baseDir: string): string {
const resolved = path.resolve(baseDir, userPath);
if (!resolved.startsWith(baseDir)) {
throw new Error("Access denied: Path traversal detected");
}
return resolved;
}
server.tool("read-file", "Read a file within the data directory", {
filename: z.string()
}, async ({ filename }) => {
const safePath = resolveSafePath(filename, "/app/data");
// Proceed with file read using safePath
});
3. Runtime Attestation and DID-Anchored Evolution
Static analysis is a snapshot. To ensure the running server matches the scanned code, implement runtime attestation. This involves generating a hash of the server artifact and anchoring it to a DID document.
Architecture Decision: Use a DID method that supports versioning, such as did:web or a blockchain-anchored DID. The DID document should contain a verificationMethod with the hash of the MCP server binary or source bundle.
// Example: Verifying server integrity at runtime
import { createHash } from "crypto";
import fetch from "node-fetch";
async function verifyServerIntegrity(didUrl: string): Promise<boolean> {
// 1. Fetch DID document
const didDoc = await fetch(didUrl).then(res => res.json());
// 2. Extract expected hash from verification method
const expectedHash = didDoc.verificationMethod[0].publicKeyMultibase;
// 3. Compute hash of current running code
const currentHash = createHash("sha256")
.update(require("fs").readFileSync(process.execPath))
.digest("hex");
// 4. Compare
return currentHash === expectedHash;
}
// Integrate check into server startup
if (!await verifyServerIntegrity("did:web:secure-server.example.com")) {
console.error("Integrity check failed: Server binary mismatch.");
process.exit(1);
}
Rationale: This approach ensures that even if an attacker modifies the running binary or injects malicious code, the attestation check will fail. The DID anchor provides a tamper-evident log of server evolution, allowing clients to verify they are interacting with a trusted version.
Pitfall Guide
-
Blind Trust in LLM-Generated Parameters
- Explanation: Developers often assume the LLM will generate safe inputs. However, prompt injection can cause the agent to pass malicious payloads to tools.
- Fix: Treat all tool inputs as untrusted. Implement strict schema validation using libraries like Zod and sanitize inputs before processing.
-
Ignoring Dependency Vulnerabilities
- Explanation: MCP servers rely on third-party packages. Vulnerabilities in dependencies can be exploited even if the server code is secure.
- Fix: Run dependency scanning (e.g.,
npm audit, Snyk) alongside mcp-security-scan. Pin dependency versions and use SBOMs.
-
Over-Reliance on Static Analysis
- Explanation: Static analysis cannot detect runtime environment manipulation or logic errors that only manifest under specific conditions.
- Fix: Complement static scans with runtime attestation and behavioral monitoring. Implement circuit breakers for tool invocations.
-
Weak Schema Validation
- Explanation: Using loose types or missing validation allows unexpected data types to reach the tool handler, potentially causing crashes or injection.
- Fix: Define explicit JSON schemas for all tool parameters. Reject requests that do not conform to the schema.
-
Exposing Internal Network Services
- Explanation: MCP tools may inadvertently allow agents to access internal services (e.g.,
localhost:8080) that should be isolated.
- Fix: Implement network segmentation and egress filtering. Restrict tool capabilities to only necessary external endpoints.
-
Failing to Rotate Secrets
- Explanation: Credentials used by MCP servers may be compromised over time. Static rotation policies are insufficient.
- Fix: Use short-lived tokens and automated secret rotation. Store secrets in a vault and inject them at runtime.
-
Lack of Audit Logging
- Explanation: Without detailed logs, it is impossible to detect or investigate security incidents involving MCP tools.
- Fix: Log all tool invocations, including parameters and responses. Ensure logs are tamper-proof and retained for compliance.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Internal Tooling | Static Scan + Schema Validation | Low risk environment; speed is priority. | Low |
| Public-Facing Agent | Static Scan + Runtime Attestation | High risk; requires integrity guarantees. | Medium |
| Enterprise Compliance | Full Stack (Scan + Attest + DID + Audit) | Regulatory requirements demand verifiable trust. | High |
| Rapid Prototyping | Static Scan Only | Fast iteration; accept higher risk temporarily. | Low |
Configuration Template
Use this GitHub Actions workflow to automate security scanning and attestation setup.
name: MCP Security Pipeline
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install Dependencies
run: npm ci
- name: Run MCP Security Scan
run: npx mcp-security-scan --fail-on-critical
- name: Generate Artifact Hash
run: |
npm run build
sha256sum dist/server.js > artifact.hash
- name: Update DID Document
if: github.ref == 'refs/heads/main'
run: |
# Script to update DID document with new hash
./scripts/update-did.sh $(cat artifact.hash)
Quick Start Guide
- Install Scanner: Run
npm install --save-dev mcp-security-scan in your MCP server project.
- Run Initial Scan: Execute
npx mcp-security-scan to identify existing vulnerabilities.
- Fix Critical Issues: Address flagged items using the secure patterns provided in the Core Solution.
- Configure CI: Add the scanner to your pipeline using the Configuration Template.
- Enable Attestation: Implement the runtime integrity check and anchor your server to a DID for production deployments.