An MCP server that provides tools to an AI model has real power to act. Without security measures, a compromised server can leak data, perform unauthorized actions, or serve as an attack vector.
server.tool(
"get_customer",
"Retrieve customer data",
{ customerId: z.string() },
async ({ customerId }, context) => {
// Check auth token from context
const token = context.meta?.authToken;
if (!token || !await verifyToken(token)) {
return {
content: [{ type: "text", text: "Error: Not authorized" }],
isError: true
};
}
const customer = await db.getCustomer(customerId);
return { content: [{ type: "text", text: JSON.stringify(customer) }] };
}
);
const permissions: Record<string, string[]> = {
"read-only": ["get_customer", "list_orders", "search_products"],
"editor": ["get_customer", "list_orders", "update_order", "create_ticket"],
"admin": ["*"] // All tools
};
function checkPermission(role: string, toolName: string): boolean {
const allowed = permissions[role];
return allowed?.includes("*") || allowed?.includes(toolName) || false;
}
Every tool input must be validated before the action is executed:
server.tool(
"execute_query",
"Execute SQL query on the database",
{
query: z.string()
.max(1000)
.refine(q => !q.toLowerCase().includes("drop"), "DROP not allowed")
.refine(q => !q.toLowerCase().includes("delete"), "DELETE not allowed")
.refine(q => q.toLowerCase().startsWith("select"), "Only SELECT queries allowed")
},
async ({ query }) => {
const result = await db.query(query);
return { content: [{ type: "text", text: JSON.stringify(result) }] };
}
);
const serverConfig = {
maxRequestsPerMinute: 60,
maxTokensPerRequest: 10000,
timeoutMs: 30000,
maxConcurrentRequests: 5
};
import { RateLimiter } from "./rate-limiter";
const limiter = new RateLimiter({ maxRequests: 60, windowMs: 60000 });
server.tool("search", "Perform search", { query: z.string() },
async ({ query }) => {
if (!limiter.allow()) {
return {
content: [{ type: "text", text: "Rate limit reached. Please wait." }],
isError: true
};
}
return { content: [{ type: "text", text: await search(query) }] };
}
);
Every action must be logged:
async function auditLog(event: {
tool: string;
input: unknown;
output: unknown;
userId: string;
timestamp: Date;
success: boolean;
}) {
await db.insert("audit_log", event);
if (!event.success) {
await alerting.notify(`Tool error: ${event.tool}`, event);
}
}
Practical tip: Security is not an afterthought — it's built in from the start. Every MCP server should have at least authentication, input validation, and audit logging. For production, add rate limiting and sandboxing.