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.