Module 7: MCP Security and Permissions
Security Is Not Optional
MCP servers can read your files, query your databases, and execute actions on your behalf. This power requires careful security consideration. In this module, you'll learn how to use MCP safely, what risks to be aware of, and how to build secure servers.
This isn't about being paranoid - it's about being thoughtful.
The Trust Model
Understanding MCP's trust boundaries is essential:
What you trust:
- The MCP client (Claude Desktop, Claude Code)
- The MCP servers you configure
- The directories/resources you expose
What you should NOT trust:
- Arbitrary MCP servers from unknown sources
- Servers with excessive permissions
- Configurations shared without review
The AI model itself doesn't bypass security - it can only use what the MCP infrastructure makes available. Your configuration defines the boundaries.
Principle of Least Privilege
The most important security principle for MCP: only grant the minimum access required.
Bad: Open access
{
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/"]
}
}
Better: Specific directories
{
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/Users/me/projects/current-project",
"/Users/me/documents/relevant-docs"
]
}
}
Best: Task-specific access
{
"project-source": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "./src"]
},
"project-docs": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "./docs"]
}
}
Sensitive Data Exposure
Be aware of what files might be in accessible directories:
High-risk files to keep out of MCP access:
.envfiles (API keys, passwords)- SSH keys (
~/.ssh/) - AWS credentials (
~/.aws/) - Browser profiles (passwords, cookies)
- Password manager databases
- Private keys and certificates
Safe practices:
- Use specific directory paths, never home directory root
- Create MCP-specific directories for file sharing
- Exclude sensitive subdirectories explicitly when possible
Database Security
For database MCP servers:
1. Use read-only connections when possible
postgresql://readonly_user:pass@host/db
Create a database user with SELECT-only permissions:
CREATE USER mcp_readonly WITH PASSWORD 'secure_password';
GRANT SELECT ON ALL TABLES IN SCHEMA public TO mcp_readonly;
2. Limit accessible tables Don't give access to tables containing:
- User passwords or hashes
- API keys or tokens
- Personal identifiable information (PII)
- Financial data
3. Use a separate database or schema Consider creating a "safe" view of your data specifically for AI access.
API Key Management
Never put API keys directly in configuration files that might be shared or committed:
Bad:
{
"github": {
"env": {
"GITHUB_TOKEN": "ghp_actualtoken123"
}
}
}
Good:
{
"github": {
"env": {
"GITHUB_TOKEN": "${GITHUB_TOKEN}"
}
}
}
Then set the environment variable securely:
# In your shell profile (not committed to git)
export GITHUB_TOKEN="ghp_actualtoken123"
Server Vetting
Before using an MCP server:
1. Check the source
- Official Anthropic servers are trusted
- Verified organization packages are generally safe
- Random npm packages need scrutiny
2. Review the code
- What permissions does it request?
- Does it phone home to external services?
- Are there any suspicious network calls?
3. Check for updates
- Is the package actively maintained?
- Are there known vulnerabilities?
- When was it last updated?
4. Read the documentation
- What data does it access?
- What side effects can it have?
- What's the permission model?
Building Secure Servers
When creating your own MCP servers:
1. Validate all inputs
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
// Validate required fields
if (!args?.path || typeof args.path !== "string") {
return {
content: [{ type: "text", text: "Invalid path parameter" }],
isError: true,
};
}
// Sanitize paths
const safePath = sanitizePath(args.path);
if (!safePath) {
return {
content: [{ type: "text", text: "Path outside allowed directories" }],
isError: true,
};
}
// Proceed with safe path
// ...
});
2. Implement path restrictions
const ALLOWED_DIRS = ["/safe/directory", "/another/safe/dir"];
function isPathAllowed(requestedPath: string): boolean {
const resolved = path.resolve(requestedPath);
return ALLOWED_DIRS.some((dir) => resolved.startsWith(dir));
}
3. Prevent command injection
// BAD - command injection vulnerability
const result = execSync(`cat ${userInput}`);
// GOOD - use proper APIs
const result = fs.readFileSync(userInput, "utf8");
// GOOD - if shell is necessary, escape properly
const result = execSync(`cat ${shellEscape(userInput)}`);
4. Limit resource consumption
// Prevent reading huge files
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
async function safeReadFile(filePath: string): Promise<string> {
const stats = await fs.stat(filePath);
if (stats.size > MAX_FILE_SIZE) {
throw new Error("File too large");
}
return fs.readFile(filePath, "utf8");
}
Network Security
For servers that make network requests:
1. Restrict allowed hosts
const ALLOWED_HOSTS = ["api.example.com", "data.example.com"];
function isHostAllowed(url: string): boolean {
const parsed = new URL(url);
return ALLOWED_HOSTS.includes(parsed.host);
}
2. Use HTTPS only
if (!url.startsWith("https://")) {
throw new Error("HTTPS required");
}
3. Validate responses Don't blindly trust API responses. Validate structure and content.
Audit Logging
For production MCP servers, implement logging:
import { appendFile } from "fs/promises";
async function auditLog(action: string, details: object) {
const entry = {
timestamp: new Date().toISOString(),
action,
details,
};
await appendFile("mcp-audit.log", JSON.stringify(entry) + "\n");
}
// In your tool handler
server.setRequestHandler(CallToolRequestSchema, async (request) => {
await auditLog("tool_call", {
tool: request.params.name,
arguments: request.params.arguments,
});
// ... handle the request
});
This lets you review what the AI has accessed and done.
Tool Approval Patterns
For sensitive operations, consider requiring confirmation:
Server-side approval flag:
{
name: "send_email",
description: "Send an email (requires confirmation)",
inputSchema: {
type: "object",
properties: {
to: { type: "string" },
subject: { type: "string" },
body: { type: "string" },
confirmed: {
type: "boolean",
description: "Set to true to confirm sending",
},
},
required: ["to", "subject", "body", "confirmed"],
},
}
Claude will need to explicitly set confirmed: true, making the action more deliberate.
Multi-step operations:
- First tool call prepares the action and returns preview
- Second tool call with confirmation executes
Configuration Security
Protect your MCP configuration:
1. File permissions
chmod 600 ~/.claude/settings.json
chmod 600 claude_desktop_config.json
2. Don't commit secrets
# .gitignore
.env
.env.local
claude_desktop_config.json
**/mcp-secrets.json
3. Use environment-specific configs
.mcp.json # Base config (committed)
.mcp.local.json # Local overrides (gitignored)
Security Checklist
Before deploying MCP servers or configurations:
- Directories are limited to what's needed
- No sensitive files in accessible paths
- Database users have minimal permissions
- API keys use environment variables
- Third-party servers are from trusted sources
- Custom servers validate all inputs
- Paths are checked against allowed directories
- No command injection vulnerabilities
- Network requests restricted to allowed hosts
- Audit logging enabled for production
- Configuration files have proper permissions
- Secrets not committed to version control
Common Vulnerabilities to Avoid
1. Path Traversal
// VULNERABLE
const file = path.join(baseDir, userInput);
// userInput = "../../../etc/passwd"
// SAFE
const file = path.join(baseDir, path.basename(userInput));
// Or validate resolved path is within baseDir
2. Command Injection
// VULNERABLE
exec(`grep ${pattern} ${file}`);
// SAFE
execFile("grep", [pattern, file]);
3. Unbounded Resource Access
// VULNERABLE - no limits
const data = await readEntireDatabase();
// SAFE - with limits
const data = await readDatabasePage(offset, 100);
4. Information Leakage
// VULNERABLE - exposes system details
catch (e) {
return { error: e.stack };
}
// SAFE - generic error
catch (e) {
return { error: "Operation failed" };
}
Key Takeaways
-
Least privilege - Only grant minimum necessary access
-
Validate inputs - Never trust data from tool calls
-
Protect secrets - Use environment variables, not hardcoded values
-
Vet servers - Review third-party servers before using
-
Audit access - Log what's being accessed and done
-
Secure configs - Proper file permissions, no secrets in git
-
Defense in depth - Multiple layers of protection
Looking Ahead
Security is an ongoing process, not a one-time setup. As you build and configure MCP servers, keep these principles in mind. In the next module, we'll cover debugging - how to troubleshoot when things go wrong.
Next up: Module 8 - Debugging MCP Connections

