Policy Cookbook
Worked examples for common mcpgw policy configurations. All examples are complete policy: blocks suitable for pasting into mcpgw.yaml.
Rules are evaluated in YAML order. The first matching rule wins; subsequent rules are not evaluated.
Example 1 — Deny dangerous tools by name
Block specific tool names entirely. The request receives HTTP 403 and JSON-RPC error -32001 policy_denied.
policy:
rules:
- id: deny-shell
action: deny
when: { tool_name: "shell_exec" }
- id: deny-system
action: deny
when: { tool_name: "system_command" }
Example 2 — Redact bearer tokens in any tool’s arguments
Strip Bearer <token> patterns from all tool call payloads before forwarding to upstream. The original value is replaced inline; the upstream never sees the token.
policy:
rules:
- id: redact-bearer
action: redact
when: { tool_name: "*" }
redact:
# v1: regex applies globally over the raw request body. jsonpath
# scoping is reserved for a future release — setting it now is rejected at startup.
- regex: 'Bearer [A-Za-z0-9._-]+'
replacement: "[REDACTED]"
Example 3 — Redact email addresses
Replace email addresses across all tool arguments. Useful when tools accept user-supplied input that may contain PII.
policy:
rules:
- id: redact-emails
action: redact
when: { tool_name: "*" }
redact:
# v1: global regex over the raw body; jsonpath reserved for a future release.
- regex: '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'
replacement: "[EMAIL]"
Example 4 — Rate-limit a write tool
Allow at most 10 calls per second to fs_write, with a burst allowance of 20. Requests that exceed the limit receive HTTP 429 and JSON-RPC error -32003 rate_limited. Rate-limit buckets are per-(rule, session): two sessions hitting the same rule have independent buckets.
policy:
rules:
- id: rl-fs-write
action: rate_limit
when: { tool_name: "fs_write" }
tokens_per_second: 10
burst: 20
Example 5 — Allow only a specific tool prefix
Use default_action: deny with explicit allow rules to run in allowlist mode. Any tool call that does not match one of the allow rules is denied with rule_id: "default_deny".
policy:
default_action: deny
rules:
- id: allow-fs-tools
action: allow
when: { tool_glob: "fs_*" }
For a fixed list of read-only tools, use tool_name_in:
policy:
default_action: deny
rules:
- id: allow-git-readonly
action: allow
when: { tool_name_in: [git_log, git_diff, git_show] }
Example 6 — Deny all server-initiated elicitations
Block elicitation/create frames before they reach the client. Requires the direction matcher, which triggers SSE-frame inspection on the response stream.
policy:
rules:
- id: no-elicitation
action: deny
when:
method: elicitation/create
direction: server_to_client
The upstream tool proceeds normally; the elicitation frame is dropped and never shown to the user. Audit records action: deny_frame. Reload with SIGHUP.
Example 7 — Redact secrets in server-to-client streams
Strip credential material from elicitation prompts and other server-initiated messages before they reach the client. The frame is forwarded, but matching byte sequences are replaced.
policy:
rules:
- id: redact-server-secrets
action: redact
when:
direction: server_to_client
redact:
- regex: 'sk-[A-Za-z0-9_-]{20,}'
replacement: "[REDACTED]"
- regex: 'Bearer [A-Za-z0-9._-]+'
replacement: "[REDACTED-BEARER]"
- regex: '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}'
replacement: "[EMAIL]"
Audit records action: redact_frame. Telemetry: mcp.frames.redacted increments per frame rewritten.
Example 8 — Allow only one server-initiated method
Permit elicitation/create from the upstream but deny any other server-initiated JSON-RPC envelope. First-match-wins: the allow rule must appear before the deny.
policy:
default_action: allow
rules:
- id: allow-elicit-create
action: allow
when:
method: elicitation/create
direction: server_to_client
- id: deny-other-server-initiated
action: deny
when:
direction: server_to_client
Example 9 — Rate-limit interactive elicitation prompts
Prevent an upstream from firing elicitations faster than users can respond. The burst value allows a short run of prompts before the limit takes effect.
policy:
rules:
- id: limit-elicit-rate
action: rate_limit
when:
method: elicitation/create
direction: server_to_client
tokens_per_second: 0.1
burst: 3
Rate-limited frames are dropped. Audit records action: rate_limit_blocked with direction: server_to_client. Telemetry: mcp.frames.denied increments per dropped frame.
For the full operator walkthrough — including verification steps and SIGHUP reload behavior — see How-to: govern elicitation/create messages.
Example 10 — Strip MCP App content from an untrusted upstream
Remove UI blocks from tool-call responses before forwarding to the client. Text blocks and other content types in result.content[] are preserved; only blocks where type == "ui" or mimeType starts with application/vnd.mcp-ui are removed.
policy:
rules:
- id: strip-untrusted-apps
action: strip_app
when:
tool_prefix: "untrusted_"
Audit records action: strip_app and stripped: N where N is the count of blocks removed. If the rule matches but the response contained no UI blocks, stripped: 0 is recorded.
Example 11 — Allow MCP Apps only from a named tool prefix
Pass UI blocks from a trusted upstream through unchanged, and strip them from everything else. First-match-wins: place the allow rule before the strip rule.
policy:
rules:
- id: allow-trusted-app-emitter
action: allow
when: { tool_prefix: "trusted_" }
- id: strip-everywhere-else
action: strip_app
when: { tool_name: "*" }
Tools prefixed trusted_ match the first rule and are forwarded with UI intact. All other tool calls match the second rule and have UI blocks stripped.
Example 12 — Monitor MCP App emission without modifying
mcpgw records mcp.app.served on every tools/call span regardless of whether a strip_app rule fires. To observe which upstreams emit MCP Apps without changing any responses, configure no strip_app rule and tail the telemetry or spans:
# Span filter — find requests where apps were served
... | jq 'select(.["mcp.app.served"] > 0) | {tool, served: .["mcp.app.served"]}'
No config change is required. Once you have a picture of upstream behaviour, apply a strip_app rule to restrict the upstreams you do not trust.
For the full operator walkthrough — including verification steps and SIGHUP reload behaviour — see How-to: govern MCP Apps content.
Caveats
Redact is regex-over-body. The regex matches content anywhere in the raw JSON-RPC request body and is replaced regardless of which JSON path the matching text lives at. The jsonpath field is rejected at startup to prevent operators from believing their redaction is path-scoped when it is not. A future release will introduce true JSONPath scoping and re-accept the field; until then, prefer regexes anchored to identifiable prefixes (Bearer , sk-, AKIA).
Deny returns -32001. Rules with action: deny produce HTTP 403 and JSON-RPC error code -32001 with message policy_denied. The audit.jsonl entry will contain "action":"deny" and the rule_id that fired.
Rate-limit returns -32003. Rules with action: rate_limit that block a request produce HTTP 429 and JSON-RPC error code -32003 with message rate_limited.
Rate-limit buckets are per-(rule, session). Two sessions calling the same rate-limited tool have independent token buckets. A burst from one session does not consume the other’s allowance.
Rule ordering matters. The first matching rule wins. Place deny rules before redact rules if you want to hard-block certain tools rather than pass them through with redacted content.