Policy rule schema

Full grammar for entries under policy.rules[]. Rules are evaluated top-down; the first match wins.

policy:
  default_action: allow      # optional: allow | deny; default allow
  rules:
    - id: <string>             # required, unique within the rules list
      action: <action>         # required: allow | deny | redact | rate_limit | strip_app
      when:                    # required: match conditions
        tool_name: <string>    # exact name, or "*" for wildcard
        # or one of: tool_prefix, tool_glob, tool_regex, tool_name_in
        method: <string>       # optional: exact JSON-RPC method (e.g. elicitation/create)
        direction: <string>    # optional: client_to_server (default) | server_to_client
      # action-specific fields below

Common fields

FieldTypeNotes
idstringUnique identifier. Appears in audit.jsonl (rule_id), spans (mcp.policy.rule_id), and metric labels. Use kebab-case.
actionenumallow, deny, redact, rate_limit, strip_app. See per-action sections below.
when.tool_namestringExact tool name (e.g. fs_read), or the wildcard "*" to match any tool call. Case-sensitive.
when.tool_prefixstringPrefix match, e.g. fs_ matches fs_read and fs_write.
when.tool_globstringShell-style glob via Go path.Match, e.g. fs_*read*.
when.tool_regexstringGo RE2 regexp. Patterns are anchored to the full tool name.
when.tool_name_inlistExact match against any listed tool name.
when.methodstringExact JSON-RPC method (e.g. elicitation/create, notifications/cancelled). Combine with direction: server_to_client to govern server-initiated SSE frames.
when.directionenumclient_to_server (default) or server_to_client. The SSE frame inspector evaluates rules with direction: server_to_client against each JSON-RPC envelope flowing back from the upstream. See how-to: govern elicitation.

The when block matches tools/call requests by default. To target other JSON-RPC methods, set when.method explicitly (and, for server→client frames, when.direction: server_to_client). Methods not covered by any rule are forwarded to default_upstream.

Within a single when: block, set at most one tool matcher. Use multiple rules when you need multiple alternatives. method and direction are independent of the tool matchers and may be combined with any of them. An empty when: block is a catch-all.


Default action

When no rule matches, policy.default_action decides the request:

policy:
  default_action: deny
  rules:
    - id: allow-readonly
      action: allow
      when: { tool_name_in: [git_log, git_diff, git_show] }
ValueBehavior
allow or absentPreserve v1 behavior: unmatched tool calls are allowed.
denyUnmatched tool calls are denied with rule_id: "default_deny".

Default-deny reuses HTTP 403 and JSON-RPC -32001 policy_denied.


action: deny

Block the request outright. Upstream is never contacted.

- id: deny-shell
  action: deny
  when: { tool_name: "shell_exec" }

Wire response:

  • HTTP 403 Forbidden
  • JSON-RPC body: {"jsonrpc":"2.0","id":<req-id>,"error":{"code":-32001,"message":"policy_denied"}}

Audit: decision: "deny", rule_id: "deny-shell".

Span: mcp.policy.decision = deny, mcp.error.kind = policy_deny, mcp.policy.rule_id = deny-shell.


action: redact

Apply regex substitutions to the raw JSON-RPC request body before forwarding upstream.

- id: redact-secrets
  action: redact
  when: { tool_name: "*" }
  redact:
    - regex: 'Bearer [A-Za-z0-9._-]+'
      replacement: "[REDACTED]"
    - regex: 'sk-[A-Za-z0-9]{20,}'
      replacement: "[REDACTED]"

redact[] is a list of substitutions. Each entry has:

FieldTypeNotes
regexstringRE2 (Go regexp) syntax. Compiled at startup; invalid regex is a fatal validation error.
replacementstringSubstitution string. Backreferences ($1, $2) are supported as in Go’s regexp.Expand.

Regex patterns are applied in order. The substituted body is what the upstream receives.

Wire response: as if the policy did nothing — the upstream’s response is forwarded verbatim. The redaction is invisible to the client.

Audit: decision: "redact", rule_id: "redact-secrets".

Span: mcp.policy.decision = redact.

The jsonpath field is reserved for v1.1 and rejected at startup in v1. See Explanation: redact vs JSONPath for why.


action: rate_limit

Token-bucket throttle. Bucket key is (rule_id, session_id).

- id: rl-fs-write
  action: rate_limit
  when: { tool_name: "fs_write" }
  tokens_per_second: 10
  burst: 20
FieldTypeNotes
tokens_per_secondfloatRefill rate. Accepts fractions (e.g. 0.0001). Must be > 0.
burstintBucket capacity. Default 1. Must be >= 1.

Sessions are identified by the Mcp-Session-Id header. Requests without that header share a single bucket per rule (the empty-session bucket).

Wire response when blocked:

  • HTTP 429 Too Many Requests
  • JSON-RPC body: {"jsonrpc":"2.0","id":<req-id>,"error":{"code":-32003,"message":"rate_limited"}}
  • HTTP header Retry-After: <seconds-until-token>

Audit: decision: "rate_limit_blocked", rule_id: "rl-fs-write".

Span: mcp.policy.decision = rate_limit_blocked, mcp.error.kind = rate_limited.


action: allow

Explicit allow. The request is forwarded unchanged. Useful when paired with default_action: deny. With the default allow mode, allow rules are mostly redundant since the absence of a matching rule already means “allow”.

- id: allow-fs-read
  action: allow
  when: { tool_name: "fs_read" }

Wire response: upstream’s response forwarded verbatim.

Audit: decision: "allow", rule_id: "allow-fs-read".


action: strip_app

Remove MCP Apps content from a tools/call response — specifically content blocks with type: "ui" and any content whose MIME type starts with application/vnd.mcp-ui+. The rest of the response is preserved. Use this when you want agents to keep calling a tool but you don’t want its interactive UI surfaces reaching the client.

- id: strip-app-fs
  action: strip_app
  when: { tool_prefix: "fs_" }

Wire response: upstream response forwarded with the offending content blocks dropped. If the entire content list was UI, the field is omitted.

Audit: decision: "strip_app", rule_id: "strip-app-fs".

Span: mcp.policy.decision = strip_app.

See how-to: govern MCP Apps content for selective allow / monitor patterns.


Server→client frames

Set direction: server_to_client to evaluate a rule against JSON-RPC envelopes flowing back from the upstream over SSE. The frame inspector runs the same engine the request path uses; only when.method typically applies (frames have no tool_name).

- id: deny-elicitation
  action: deny
  when:
    direction: server_to_client
    method: elicitation/create

A deny decision drops the frame from the SSE stream (the client never sees it). redact rewrites the frame body. rate_limit throttles per (rule_id, session_id). See how-to: govern elicitation for the canonical patterns.


Wildcard semantics

tool_name: "*" matches every tools/call. It does NOT match other methods (tools/list, initialize).

tool_glob supports *, ?, and character classes such as [fg]s_*. Tool names are treated as flat, case-sensitive strings.

tool_regex uses Go regexp syntax and is anchored to the full tool name. tool_regex: "db_(select|describe)_.+" behaves like ^db_(select|describe)_.+$.


Ordering and shadowing

Rules are evaluated top-down. The first rule whose when matches is applied; subsequent rules are not evaluated.

This is important for two common patterns:

1. Hard-deny before wildcard redact. If you put redact with tool_name: "*" above a deny rule, the wildcard captures the call and the deny never fires. Always order denies first.

# CORRECT
- { id: deny-shell, action: deny, when: { tool_name: "shell_exec" } }
- { id: redact-all, action: redact, when: { tool_name: "*" }, redact: [...] }

# WRONG — deny-shell is shadowed and never fires
- { id: redact-all, action: redact, when: { tool_name: "*" }, redact: [...] }
- { id: deny-shell, action: deny, when: { tool_name: "shell_exec" } }

2. Specific rate-limit before wildcard redact. Same logic — a wildcard above your rate-limit rule means redact-and-forward instead of throttling.

The mcpgw server startup logs print the loaded rule order on every reload so you can verify visually.


Validation summary

The startup validator enforces:

  • Each id is unique
  • policy.default_action is allow, deny, or absent
  • action is one of allow, deny, redact, rate_limit, strip_app
  • when.direction is empty, client_to_server, or server_to_client
  • At most one of tool_name, tool_prefix, tool_glob, tool_regex, and tool_name_in is set per when
  • tool_glob parses as a valid glob
  • tool_regex compiles as Go regexp
  • tool_name_in is non-empty when set
  • For action: redact, redact[] is non-empty and each regex compiles
  • For action: rate_limit, tokens_per_second > 0
  • jsonpath is not present in any rule (reserved)