The uncomfortable truth: An MCP server with full database access means your AI can DROP TABLE. Without proper security, MCP is a footgun. Here’s how to do it safely.
The Risk Spectrum
- Low risk: Read-only access to public data (documentation, search)
- Medium risk: Read access to internal data (databases, files, analytics)
- High risk: Write access to anything (creating PRs, sending emails, modifying data)
- Critical risk: Infrastructure access (deploying code, managing servers, deleting resources)
Rule #1: Principle of Least Privilege
Never give an MCP server more access than the specific task needs.
Bad: Database MCP with root credentials (can read/write/delete anything)
Good: Database MCP with a read-only user that can only access specific tables
-- Create a restricted database user for MCP
CREATE USER mcp_readonly WITH PASSWORD 'xxx';
GRANT SELECT ON users, orders, products TO mcp_readonly;
-- NO INSERT, UPDATE, DELETE granted
Rule #2: Scope API Keys
When connecting to external services:
- GitHub: Create a fine-grained token with only the repos and permissions needed
- AWS: Use an IAM role with minimal policy, not admin credentials
- Stripe: Use restricted keys (read-only for analytics, limited write for specific operations)
Rule #3: Add Confirmation for Destructive Actions
Your MCP server should require human confirmation before:
- Deleting anything
- Sending emails/messages
- Deploying to production
- Modifying financial data
- Changing permissions
server.tool("delete_record", { id: z.string() }, async ({ id }) => {
// Return confirmation prompt instead of executing
return {
content: [{ type: "text",
text: `CONFIRMATION REQUIRED: Delete record ${id}? This cannot be undone. Reply 'confirm delete ${id}' to proceed.`
}]
};
});
Rule #4: Audit Everything
Log every tool call with: timestamp, who triggered it, what was called, what parameters were passed, what was returned.
// Wrap every tool with audit logging
function auditedTool(name, schema, handler) {
return server.tool(name, schema, async (args) => {
console.log(JSON.stringify({ ts: Date.now(), tool: name, args }));
const result = await handler(args);
console.log(JSON.stringify({ ts: Date.now(), tool: name, result: "completed" }));
return result;
});
}
Rule #5: Don’t Expose in Production Without Auth
Remote MCP servers (HTTP transport) MUST have authentication. Anyone who knows the URL can call your tools otherwise.
- Use OAuth2 for remote MCP servers
- Use API key validation in middleware
- Never expose on public internet without auth
Common MCP Security Mistakes
- Using personal access tokens instead of scoped service accounts
- Giving write access when only read is needed
- Not rotating credentials after setup
- Running MCP servers as root/admin
- Trusting AI output without validation for critical operations
The Safe Default
Start with read-only access. Prove the use case works. Then selectively add write permissions for specific operations with human-in-the-loop confirmation.