Govern elicitation/create messages

Problem: upstream MCP servers can send elicitation/create messages — server-initiated requests that pause a tool and ask the end user for input. You want to block those requests from specific upstreams, strip PII from prompts before users see them, or limit how frequently a misbehaving server can solicit input.

Solution: use the method and direction matchers on a policy rule. Any rule with direction: server_to_client operates on SSE frames coming back from the upstream rather than on inbound requests from the client.

How it works

Elicitation messages travel upstream → mcpgw → client over the SSE response stream. When direction: server_to_client rules are present, mcpgw’s SSE inspector parses every data: line, and any frame whose content is a JSON-RPC envelope is evaluated by the policy engine.

client                  mcpgw                    upstream
  │  POST /mcp tools/call  │  POST /mcp tools/call    │
  │ ────────────────────► │ ────────────────────►    │
  │                        │                          │
  │                        │  ◄ event-stream (SSE)    │
  │                        │  data: {"method":"elicitation/create",...}
  │                        │     │ policy.Evaluate(direction=server_to_client)
  │                        │     ▼ deny → drop frame
  │                        │     ▼ redact → rewrite frame
  │                        │     ▼ allow → forward
  │  ◄ event-stream         │
  │ ◄────────────────────  │

The existing inbound path (client-to-server) is unaffected. Inbound rules continue to operate as before; outbound rules run as a second pass on the SSE response.

Deny all elicitations from an upstream

The simplest case: refuse interactive prompts entirely. The upstream’s tool call proceeds, but the elicitation frame never reaches the client.

policy:
  rules:
    - id: no-elicitation-from-untrusted
      action: deny
      when:
        method: elicitation/create
        direction: server_to_client

After SIGHUP, any elicitation/create frame is dropped before reaching the client. From the upstream tool’s perspective, the user simply never responded to the prompt. The audit log records:

{"action":"deny_frame","rule_id":"no-elicitation-from-untrusted","method":"elicitation/create","direction":"server_to_client","session_id":"...","ts":"..."}

Redact PII out of elicitation prompts

If you want users to see elicitations but not have upstream context leak credential material or PII, use action: redact:

policy:
  rules:
    - id: redact-elicit-secrets
      action: redact
      when:
        method: elicitation/create
        direction: server_to_client
      redact:
        - regex: 'sk-[A-Za-z0-9_-]{20,}'
          replacement: "[REDACTED]"
        - regex: '\b\d{3}-\d{2}-\d{4}\b'
          replacement: "[SSN-REDACTED]"
        - regex: '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}'
          replacement: "[EMAIL]"

The regex patterns are applied globally over the raw frame data: bytes before forwarding. The rest of the SSE envelope (event:, id:, blank lines) is preserved intact. Audit records action: redact_frame.

Redact is regex-over-bytes: patterns apply anywhere in the raw JSON-RPC frame. See Explanation: redact vs JSONPath for why v1 does not offer path-scoped redaction.

Allow only known-safe server-initiated methods

If an upstream uses elicitation/create legitimately for confirmations but you want to block other server-initiated methods, allow elicitation/create explicitly and deny everything else in that direction:

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

Rules are first-match-wins. elicitation/create frames hit the first rule and are forwarded; any other server-initiated JSON-RPC envelope hits the second rule and is dropped.

Rate-limit interactive prompts

An upstream that fires elicitations in a tight loop can pin the user in a prompt cycle. Use action: rate_limit with a conservative refill rate:

policy:
  rules:
    - id: limit-elicit-rate
      action: rate_limit
      when:
        method: elicitation/create
        direction: server_to_client
      tokens_per_second: 0.1
      burst: 3

This allows three elicitations back-to-back and then one every ten seconds per session. Rate-limited frames are dropped and audit as action: rate_limit_blocked with direction: server_to_client.

Verifying it works

Send a request that causes an upstream to emit an elicitation. Watch the audit log filtered to the server-to-client direction:

tail -f /var/log/mcpgw/audit.jsonl | jq 'select(.direction == "server_to_client")'

Expected action values:

actionMeaning
allowFrame passed through — no rule matched, or a matching allow rule fired.
deny_frameFrame was dropped by a deny rule.
redact_frameFrame’s data: bytes were rewritten by a redact rule.
rate_limit_blockedFrame was dropped because the rate-limit bucket was empty.

Telemetry spans carry matching counters. On the proxy span for the call:

mcp.frames.total = 5
mcp.frames.denied = 1
mcp.frames.redacted = 2
mcp.direction = server_to_client

Limitations

Frame-level only. Policy evaluation operates on individual JSON-RPC envelopes inside SSE frames. Comment-only heartbeat lines (bare :) and non-JSON-RPC data: lines pass through unconditionally — the engine has nothing to match against.

Direction is binary. client_to_server (inbound requests) and server_to_client (SSE frames) are the only two values. There is no per-tool or per-upstream variation at the direction level.

Redact rewrites bytes, not semantics. If a redaction regex does not match the actual format used by the upstream, the frame is forwarded unchanged. Test patterns against real upstream responses before deploying.

SIGHUP required for rule changes. Adding or removing direction: server_to_client rules takes effect on the next SIGHUP (docker kill --signal=HUP mcpgw). In-flight SSE streams continue under the old policy until they end naturally.