Skilllibrary mcp-security-permissions
Secure MCP servers with least privilege, tool gating, input validation, and fail-closed behavior. Use when hardening an MCP server for production, implementing per-tool permission controls, validating tool inputs against injection attacks, or auditing an MCP server's security posture.
git clone https://github.com/merceralex397-collab/skilllibrary
T=$(mktemp -d) && git clone --depth=1 https://github.com/merceralex397-collab/skilllibrary "$T" && mkdir -p ~/.claude/skills && cp -r "$T/07-mcp/mcp-security-permissions" ~/.claude/skills/merceralex397-collab-skilllibrary-mcp-security-permissions && rm -rf "$T"
07-mcp/mcp-security-permissions/SKILL.mdPurpose
Harden MCP servers for production use. Covers the MCP-specific security model: tool annotations for trust signaling, input validation, path restrictions, prompt injection defense, least-privilege principles, and the security requirements in the MCP specification.
When to use this skill
- Hardening an MCP server for production deployment
- Implementing per-tool or per-tenant permission controls
- Adding input validation and injection prevention to tool handlers
- Auditing an MCP server's security posture
- Understanding MCP's trust model between hosts, clients, and servers
Do not use this skill when
- Implementing OAuth auth flows → use
mcp-auth-transports - Designing multi-tenant isolation → use
mcp-multi-tenant-design - Building a dev/prototype server with no production security needs
MCP security model
Trust boundaries
User ←→ Host (AI app) ←→ MCP Client ←→ MCP Server ←→ External Services ↑ ↑ ↑ Trust boundary 1 Trust boundary 2 Trust boundary 3
- Hosts SHOULD show users which tools are available and get confirmation before tool invocations
- Clients MUST consider tool annotations as untrusted unless from trusted servers
- Servers MUST validate all inputs — they cannot trust that the LLM or client sent valid data
Tool annotations for trust signaling
Tool annotations help hosts make safe decisions about tool invocation:
{ "name": "delete_file", "annotations": { "readOnlyHint": false, "destructiveHint": true, "idempotentHint": false, "openWorldHint": false } }
| Annotation | Purpose | Example |
|---|---|---|
| Tool doesn't modify state | Search, list, read operations |
| Tool makes irreversible changes | Delete, drop, overwrite operations |
| Safe to retry | Update with same input = same result |
| Interacts with external world | Sends emails, makes API calls |
Important: These are hints, not enforced constraints. The host decides whether to require user confirmation based on these hints.
Operating procedure
Phase 1 — Input validation
Every tool handler must validate inputs before processing:
server.tool("read_file", { path: z.string() }, async ({ path }) => { // 1. Validate path is within allowed directory const resolved = path.resolve(path); if (!resolved.startsWith(ALLOWED_BASE_DIR)) { throw new McpError(ErrorCode.InvalidParams, "Path outside allowed directory"); } // 2. Prevent path traversal if (path.includes("..")) { throw new McpError(ErrorCode.InvalidParams, "Path traversal not allowed"); } // 3. Check file exists and is readable // ... proceed with operation });
Common input validation checks:
- Path traversal (
) in file paths../ - SQL injection in database query parameters
- Command injection in shell-executing tools
- URI validation for resource URIs
- Size limits on string inputs
- Type coercion (ensure numbers are actually numbers)
Phase 2 — Least privilege
- Request only the API scopes your tools actually need
- If a tool only reads data, use read-only API credentials
- Separate read and write operations into different tools so read-only deployments can exclude write tools
- Use tool annotations to signal intent:
for read toolsreadOnlyHint: true
Phase 3 — Prompt injection defense
LLMs can be manipulated to call tools with malicious arguments. Defend at the server level:
- Never trust tool arguments as safe — validate and sanitize everything
- Parameterize queries — never concatenate user input into SQL/shell/URL strings
- Restrict tool scope — a "search" tool should not also write files
- Log all tool calls — enable audit trail via the MCP
capabilitylogging
@mcp.tool() async def search_database(query: str, table: str) -> str: """Search a database table.""" # ✅ Parameterized query if table not in ALLOWED_TABLES: raise McpError("Table not in allowlist") results = await db.execute("SELECT * FROM ? WHERE content LIKE ?", [table, f"%{query}%"]) # ❌ NEVER do this — SQL injection # results = await db.execute(f"SELECT * FROM {table} WHERE content LIKE '%{query}%'")
Phase 4 — Transport-level security
stdio servers:
- Inherit OS process permissions — run as restricted user
- Do not expose stdio servers over network
- Credentials come from environment, not from the protocol
Streamable HTTP servers:
- MUST validate
header to prevent DNS rebindingOrigin - MUST bind to
(not127.0.0.1
) for local servers0.0.0.0 - MUST use HTTPS for production remote servers
- MUST implement OAuth 2.1 for remote authentication
- SHOULD implement rate limiting per session
Phase 5 — Audit and logging
Enable the
logging capability and log security-relevant events:
server.sendLoggingMessage({ level: "warning", data: `Tool 'delete_file' called with path: ${path} by session: ${sessionId}` });
Decision rules
- All tool inputs are untrusted. Validate every parameter, even from "trusted" hosts.
- Tool annotations are hints, not enforcement. Hosts decide how to use them.
- Fail closed: if validation is uncertain, reject the request rather than proceeding.
- Separate read and write tools so deployments can restrict to read-only.
- Never concatenate user input into queries/commands — always parameterize.
- Log all destructive operations for audit trail.
Output requirements
- Input validation on all tool handlers
- Tool annotations set correctly (readOnlyHint, destructiveHint, etc.)
- Least-privilege API credentials
- Transport security hardening (Origin validation, HTTPS, binding)
- Audit logging for destructive operations
Related skills
— OAuth 2.1 and transport securitymcp-auth-transports
— per-tenant security isolationmcp-multi-tenant-design
— designing tools with security in mindmcp-tool-design
— testing security controlsmcp-inspector-debugging
Failure handling
- If a tool receives invalid input, return a clear error message without revealing internal details
- If authentication fails, return 401 with
header (HTTP) or error message (stdio)WWW-Authenticate - If a security audit reveals vulnerabilities, prioritize: injection prevention > path traversal > privilege escalation > information disclosure