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_name is exact-match. "shell_*" is not a wildcard — only the literal * is. Use tool_glob: "shell_*" or tool_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/call method). A deny rule cannot block initialize, tools/list, resources/read, or other MCP protocol methods. Those are routed by default_upstream and 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.