Deny a tool by name
Problem: an upstream MCP server exposes a tool you do not want clients to call — shell_exec, system_command, eval_code, or any tool whose blast radius makes you nervous.
Solution: add an action: deny rule whose when.tool_name matches the exact tool name. Place it before any wildcard rules so it cannot be shadowed.
Recipe
policy:
rules:
- id: deny-shell
action: deny
when: { tool_name: "shell_exec" }
- id: deny-system
action: deny
when: { tool_name: "system_command" }
Reload with SIGHUP (docker kill --signal=HUP mcpgw) — no restart needed.
What happens
A blocked call gets HTTP 403 and JSON-RPC error -32001 with message policy_denied. The upstream MCP server is never contacted. The audit log records:
{ "decision": "deny", "rule_id": "deny-shell", "tool": "shell_exec", "session_id": "..." }
In Datadog APM, the span has mcp.policy.decision = deny and mcp.error.kind = policy_deny.
Pitfalls
tool_nameis exact-match."shell_*"is not a wildcard — only the literal*is. Usetool_glob: "shell_*"ortool_prefix: "shell_"for pattern matching.- First match wins. If you put a
tool_name: "*"rule above the deny rule, the wildcard captures every call and the deny never fires. Deny rules go first. - Deny is for tool calls (
tools/callmethod). Adenyrule cannot blockinitialize,tools/list,resources/read, or other MCP protocol methods. Those are routed bydefault_upstreamand are not subject to per-tool policy.
Denying everything by default
For allowlist mode, set policy.default_action: deny and add explicit allow rules:
policy:
default_action: deny
rules:
- id: allow-readonly-git
action: allow
when: { tool_name_in: [git_log, git_diff, git_show] }
See How-to: enable default-deny for the full rollout recipe.