MCP Security
MCP gives agents the ability to call your tools. That is the same trust boundary as giving a junior engineer shell access — design for least privilege from day one.
Last reviewed: June 2026
MCP auth specs and SDK APIs evolve. Check modelcontextprotocol.io and your client's MCP settings before production deployment.
Threat model
| Threat | Example | Mitigation |
|---|---|---|
| Secret exfiltration | API key in mcp.json committed to git | Env vars only; ${env:VAR} in config |
| Over-privileged tools | run_sql accepts arbitrary queries | Parameterized tools; read-only DB user |
| Prompt injection → tool abuse | User message tricks agent into destructive call | Human confirmation for writes; tool allowlists |
| Data leakage in responses | Tool returns full user PII | Return minimal fields; redact in handler |
| Supply chain | Unvetted third-party MCP server | Org allowlist; pin server versions |
| Lateral movement | Staging MCP creds work on production | Separate servers per environment |
See also Security and Prompt Injection for user-facing LLM features and Security Anti-patterns for coding mistakes.
Secrets in client config
Never hardcode credentials in .cursor/mcp.json or Claude Code MCP settings:
{
"mcpServers": {
"staging-db": {
"command": "node",
"args": ["./mcp-server/dist/index.js"],
"env": {
"DATABASE_URL": "${env:DATABASE_URL}",
"API_TOKEN": "${env:INTERNAL_API_TOKEN}"
}
}
}
}
| Do | Don't |
|---|---|
${env:VAR} references | Literal sk-... or connection strings in JSON |
.env.local gitignored | .env committed "for convenience" |
| Rotate on any leak | Reuse prod creds in dev MCP |
Add .cursor/mcp.json to PR review when it changes — same scrutiny as CI config.
Server-side auth patterns
Read-only database user
Create a DB role with SELECT only on approved tables:
CREATE USER mcp_readonly WITH PASSWORD '...';
GRANT CONNECT ON DATABASE staging TO mcp_readonly;
GRANT SELECT ON users, orders TO mcp_readonly;
-- No INSERT, UPDATE, DELETE, DDL
Wire DATABASE_URL to that user in the MCP server's environment.
API token scoping
| Scope | Token permissions |
|---|---|
| Docs search | Read-only search API |
| Ticket lookup | Read issue by ID |
| Ticket create | Separate tool; requires confirmation param |
Issue short-lived tokens per developer or per MCP server instance — not org-wide admin keys.
OAuth for remote MCP (2026)
Remote MCP servers may require OAuth 2.1 flows. The client opens a browser for consent; tokens refresh automatically. Verify your IDE's support — behavior differs between Cursor, Claude Code, and Codex.
Docs: MCP authorization spec (check current draft).
Tool design for security
server.tool(
"lookup_user_by_email",
{ email: z.string().email().max(254) },
async ({ email }) => {
const user = await db.users.findByEmail(email);
if (!user) return { content: [{ type: "text", text: "Not found" }] };
// Return minimal fields — not full row
return {
content: [{
type: "text",
text: JSON.stringify({ id: user.id, status: user.status }),
}],
};
}
);
| Principle | Implementation |
|---|---|
| Validate all inputs | Zod schemas; reject unknown fields |
| One action per tool | lookup_user, not manage_user |
| No raw SQL from model | Fixed queries with bound parameters |
| Cap response size | Pagination; truncate long JSON |
| Destructive ops gated | Require explicit confirm: true param; log caller |
Transport security
| Transport | Typical use | Notes |
|---|---|---|
| stdio | Local dev; client spawns process | Process inherits env; isolate with dedicated user |
| SSE / HTTP | Remote shared servers | TLS required; auth on every request |
| Docker | Team-standardized servers | Pin image digest; no --privileged |
For remote servers, terminate TLS at the edge and authenticate with OAuth or mTLS — not IP allowlists alone.
Audit and logging
Log at the MCP server, not only in the IDE:
async function auditLog(tool: string, argsHash: string, userId: string) {
await logger.info({ tool, argsHash, userId, ts: Date.now() });
}
| Log | Do not log |
|---|---|
| Tool name, timestamp, user/session ID | Full request bodies with PII |
| Hashed argument fingerprint | Raw API keys or tokens |
| Success / failure status | Entire DB query results |
Ship logs to your SIEM for production-adjacent servers (staging DB, internal APIs).
Team policy
| Policy | Recommendation |
|---|---|
| Allowlist | Only approved MCP servers in Team AI Policy |
| No personal servers | Ban ad-hoc MCP on repos with prod secrets |
| PR review | mcp.json + server code require platform team approval |
| Environment separation | Different servers and creds for dev / staging / prod |
| Offboarding | Revoke tokens when developer leaves |
Pre-production checklist
- [ ] Read-only credentials for exploration tools
- [ ] Inputs validated with schema (Zod / Pydantic)
- [ ] No secrets in repo or tool responses
- [ ] Write tools require explicit confirmation
- [ ] Audit log enabled
- [ ]
.cursorignoreexcludes.env*, keys, certs - [ ] Server tested with MCP Inspector before IDE connect
- [ ] Incident runbook if token leaked (rotate, revoke, notify)