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
| Field | Type | Notes |
|---|---|---|
id | string | Unique identifier. Appears in audit.jsonl (rule_id), spans (mcp.policy.rule_id), and metric labels. Use kebab-case. |
action | enum | allow, deny, redact, rate_limit, strip_app. See per-action sections below. |
when.tool_name | string | Exact tool name (e.g. fs_read), or the wildcard "*" to match any tool call. Case-sensitive. |
when.tool_prefix | string | Prefix match, e.g. fs_ matches fs_read and fs_write. |
when.tool_glob | string | Shell-style glob via Go path.Match, e.g. fs_*read*. |
when.tool_regex | string | Go RE2 regexp. Patterns are anchored to the full tool name. |
when.tool_name_in | list | Exact match against any listed tool name. |
when.method | string | Exact JSON-RPC method (e.g. elicitation/create, notifications/cancelled). Combine with direction: server_to_client to govern server-initiated SSE frames. |
when.direction | enum | client_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] }
| Value | Behavior |
|---|---|
allow or absent | Preserve v1 behavior: unmatched tool calls are allowed. |
deny | Unmatched 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:
| Field | Type | Notes |
|---|---|---|
regex | string | RE2 (Go regexp) syntax. Compiled at startup; invalid regex is a fatal validation error. |
replacement | string | Substitution 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
| Field | Type | Notes |
|---|---|---|
tokens_per_second | float | Refill rate. Accepts fractions (e.g. 0.0001). Must be > 0. |
burst | int | Bucket 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
idis unique policy.default_actionisallow,deny, or absentactionis one ofallow,deny,redact,rate_limit,strip_appwhen.directionis empty,client_to_server, orserver_to_client- At most one of
tool_name,tool_prefix,tool_glob,tool_regex, andtool_name_inis set perwhen tool_globparses as a valid globtool_regexcompiles as Go regexptool_name_inis non-empty when set- For
action: redact,redact[]is non-empty and eachregexcompiles - For
action: rate_limit,tokens_per_second > 0 jsonpathis not present in any rule (reserved)