Redact secrets in tool arguments

Problem: clients sometimes paste API keys, bearer tokens, or other secrets into tool arguments. You want those secrets stripped before they reach the upstream.

Solution: add an action: redact rule with one or more regex patterns. The regex is applied to the raw JSON-RPC request body; matches are replaced inline.

Recipe — the common cases

policy:
  rules:
    - id: redact-secrets
      action: redact
      when: { tool_name: "*" }     # apply to every tool call
      redact:
        # Bearer tokens (Authorization headers, JWTs in args, etc.)
        - regex: 'Bearer [A-Za-z0-9._-]+'
          replacement: "[REDACTED-BEARER]"
        # OpenAI-style API keys
        - regex: 'sk-[A-Za-z0-9]{20,}'
          replacement: "[REDACTED-OPENAI]"
        # AWS access key IDs
        - regex: 'AKIA[0-9A-Z]{16}'
          replacement: "[REDACTED-AWS]"
        # Generic JWT shape (header.payload.signature, base64url)
        - regex: 'eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+'
          replacement: "[REDACTED-JWT]"
        # Email addresses (PII redaction)
        - regex: '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}'
          replacement: "[EMAIL]"

What happens

The original request body is read into memory, the regex set is applied in order, and the rewritten body is what the upstream receives. The audit log records decision: "redact" and the rule id; the span in Datadog records mcp.policy.decision = redact.

The original payload is not logged anywhere by mcpgw. The audit log only records that redaction occurred, not what was redacted. If you need to see what was stripped you must add upstream logging — and then think hard about whether you want to.

Pitfalls

  • Regex is global over the raw JSON-RPC body in v1. It will match secrets at any JSON path, including method names, ids, and metadata. Test your regex against realistic payloads. JSONPath scoping is reserved for v1.1; setting the jsonpath field today is rejected at startup so you never accidentally believe redaction is path-scoped when it is not.
  • Order your patterns carefully. Earlier patterns can consume substrings that later patterns need. If the JWT regex sees Bearer eyJ... after the bearer regex has already replaced it with [REDACTED-BEARER], the JWT regex no longer matches.
  • Anchor permissive patterns. A regex like [A-Za-z0-9]{32,} will match any long alphanumeric string, including tool names and resource IDs. Use a meaningful prefix or character class.
  • Redact is not encryption. The audit log proves redaction occurred, but a misconfigured regex can still let secrets through. Treat this as defense in depth, not a primary control.

Verifying it works

curl -s -X POST http://localhost:7332/mcp \
  -H "Content-Type: application/json" \
  -H "Mcp-Session-Id: redact-test" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"fs_read","arguments":{"path":"/etc/hosts","auth":"Bearer abc.def.ghi"}}}'

# In another terminal, watch the upstream see the rewritten body:
docker logs -f mcp-fs    # or however your upstream surfaces request bodies

The upstream should see "auth": "[REDACTED-BEARER]" rather than the original token.