Feature: support argument-level patterns in MCP tool permission rules (parity with Bash rules)

Resolved 💬 2 comments Opened Feb 27, 2026 by Capiru Closed Mar 28, 2026

Summary

Bash permission rules support argument-level glob patterns:

"Bash(gh api:*)"
"Bash(uv run pytest:*)"

MCP tool permission rules support only tool-level matching — no argument patterns:

"mcp__playwright__browser_navigate"   // allowed
"mcp__playwright__browser_navigate(http://localhost:*)"  // ❌ rejected by schema validation

Desired behaviour

Allow MCP rules to accept the same parenthesised argument pattern syntax that Bash rules use, so users can express fine-grained restrictions like:

"mcp__playwright__browser_navigate(http://localhost:*)"
"mcp__playwright__browser_tabs(action: select*)"

This would let project settings.json / settings.local.json restrict an MCP tool both by name (which tool may be called) and by argument (what values it may be called with), without requiring a separate PreToolUse hook.

Current workaround

The only way to enforce argument-level restrictions today is a PreToolUse hook. Getting it right has two non-obvious pitfalls:

  1. Exit code must be 0, not 1 or 2 — the block decision is communicated via JSON on stdout using hookSpecificOutput.permissionDecision: "deny". Exit code 2 also blocks but swallows the reason message.
  2. The tool input is nested under tool_input in the stdin JSON, not at the top level.

Working example that restricts browser_navigate to localhost and browser_tabs to safe actions only:

"hooks": {
  "PreToolUse": [
    {
      "matcher": "mcp__playwright__browser_navigate",
      "hooks": [{
        "type": "command",
        "command": "python3 -c \"import sys,json; d=json.load(sys.stdin); u=d.get('tool_input',d).get('url',''); (print(json.dumps({'hookSpecificOutput':{'hookEventName':'PreToolUse','permissionDecision':'deny','permissionDecisionReason':'browser_navigate is restricted to localhost URLs. Attempted: '+u}})) or None) if 'localhost' not in u else None; sys.exit(0)\""
      }]
    },
    {
      "matcher": "mcp__playwright__browser_tabs",
      "hooks": [{
        "type": "command",
        "command": "python3 -c \"import sys,json; d=json.load(sys.stdin); a=d.get('tool_input',d).get('action',''); (print(json.dumps({'hookSpecificOutput':{'hookEventName':'PreToolUse','permissionDecision':'deny','permissionDecisionReason':'browser_tabs action not permitted: '+a+'. Allowed: select, list, new'}})) or None) if a not in ('select','list','new') else None; sys.exit(0)\""
      }]
    }
  ]
}

This works and produces a clear denial message (e.g. "browser_navigate is restricted to localhost URLs. Attempted: https://example.com"), but it is verbose, fragile (inline Python in JSON-escaped strings), and requires knowing the undocumented tool_input nesting and the hookSpecificOutput JSON contract.

The equivalent intent expressed as a permission rule would be two readable lines:

"mcp__playwright__browser_navigate(http://localhost:*)",
"mcp__playwright__browser_tabs(action: select*, list*, new*)"

Use cases

Argument-level restrictions are useful any time you want to auto-approve a tool but constrain what values it can be called with:

| MCP server | Tool | Desired restriction |
|------------|------|---------------------|
| Playwright | browser_navigate | Localhost URLs only during local E2E testing |
| Playwright | browser_tabs | Safe actions only (select, list, new) — not close |
| Filesystem | write_file | Paths within the project directory only |
| Database | execute_query | SELECT statements only (no INSERT/UPDATE/DELETE) |
| GitHub | create_issue / push | Specific repos only |
| Email / Slack | send_message | Internal domain recipients only |

In every case the tool itself is trusted — only certain argument values should be auto-approved without prompting the user.

Proposed syntax

Follow the existing Bash(pattern) convention exactly:

| Rule | Meaning |
|------|---------|
| mcp__playwright__browser_navigate | Allow any URL (current behaviour) |
| mcp__playwright__browser_navigate(http://localhost:*) | Allow localhost URLs only |
| mcp__playwright__browser_tabs(action: select*) | Allow select/new/list only |
| mcp__playwright__* | Allow all tools from server (current behaviour) |

Discovered while setting up Playwright MCP for Flutter web E2E testing.

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗