In enforce mode MCP Shield answers some messages itself. This page lists every answer, why it has that shape, and the event each one leaves. In log mode none of these answers are sent: the message passes and only the event is recorded, with blocked: false.
Why most blocks are JSON-RPC errors
MCP clients treat an HTTP error status as a transport failure: the call in progress fails, and some clients drop or retry the whole session. A JSON-RPC error carrying the id of the request is delivered to that one pending call instead. The assistant shows “blocked by OpenWAF MCP Shield: …” for that tool call, and the session keeps working.
So policy and inspection blocks answer HTTP 200 with a JSON-RPC error. An HTTP error status is only used when there is no request id to answer (a body that cannot be parsed) or when the problem is the client as a whole (rate and size).
Every answer
| Situation (enforce) | HTTP | JSON-RPC code | Event kind | Detection id |
|---|---|---|---|---|
Over rate_per_token requests per minute | 429 with Retry-After | -32000 | mcp_rate_limited | mcp.rate_limited |
Body over max_request_kb | 413 | -32600 | mcp_invalid | mcp.too_large |
| Body is not JSON | 400, id null | -32700 | mcp_invalid | mcp.parse_error |
| JSON but not JSON-RPC 2.0, or an empty batch | 400, id null | -32600 | mcp_invalid | mcp.invalid_request |
Method not in methods_allowed | 200 | -32601 | mcp_blocked_method | mcp.method_not_allowed |
Tool in tools_denied, or missing from a non-empty tools_allowed | 200 | -32602 | mcp_blocked_tool | mcp.tool_denied, mcp.tool_not_allowed |
Injection or credential in the request, inspect_requests: block | 200 | -32001 | mcp_injection_request, mcp_dlp | The detector ids |
Injection or credential in the response, inspect_responses: block | Your server's status | -32001, replacing that response message (same id; in a stream, that event) | mcp_injection_response, mcp_dlp | The detector ids |
| A blocked notification | 202, empty body | — | mcp_blocked_method | mcp.method_not_allowed |
Requests that the server sends to the client (sampling/createMessage, elicitation/create) are only logged, never replaced.
What the client receives
The message is blocked by OpenWAF MCP Shield: followed by the detection ids, separated by commas. error.data.openwaf repeats the event kind and the ids, so a client or an agent can tell a shield block from an error of your server:
{"jsonrpc": "2.0", "id": 7,
"error": {"code": -32602, "message": "blocked by OpenWAF MCP Shield: mcp.tool_denied",
"data": {"openwaf": {"kind": "mcp_blocked_tool", "detections": ["mcp.tool_denied"]}}}}A tool result replaced because of a detection looks the same, with code -32001, the id of the original request and the detector ids, for example blocked by OpenWAF MCP Shield: pi.override.en, exfil.markdown_image. The rate limit answers -32000 with the message rate limit exceeded (OpenWAF MCP Shield).
Batches
If any message of a batch is blocked, the edge answers the whole batch and nothing reaches your server. The answer is a 200 array: each blocked message gets its own error, and every other request with an id gets -32600 batch rejected by OpenWAF MCP Shield. A batch with no request ids (notifications only) gets 202.
Events
Each blocked or detected message leaves an event in the Bots and AI agents events, one per kind and message (sampled to at most 200 per site per minute):
| Kind | When |
|---|---|
mcp_blocked_method | A method outside methods_allowed |
mcp_blocked_tool | A tool denied or not allowed |
mcp_injection_request | A prompt-injection detector matched the request |
mcp_injection_response | A prompt-injection detector matched the response |
mcp_dlp | A credential pattern matched the request or the response |
mcp_rate_limited | The per-token limit was exceeded |
mcp_invalid | The body was too large, not JSON or not JSON-RPC |
Every event has the detections (id, label and a snippet of at most 160 bytes, redacted for credentials) and an mcp object:
| Field | What it is |
|---|---|
method | The JSON-RPC method, empty when unknown (invalid body, GET stream) |
tool | The tool name of tools/call, the URI of resources/read (shortened) or the prompt name |
token_fp | A fingerprint of the Authorization header: the first 8 bytes of its SHA-256, in hex. Never the token |
blocked | true when the edge answered or replaced the message, false when it was only logged |
Each one is also a security event with the rule name MCP Shield, action block or log, in the live events view and in AI Crawl Control → MCP Shield. From the CLI:
openwaf-cloud ctl ai report example.com --view events --kind mcp_injection_response,mcp_dlp --limit 50Counters
Per endpoint, the report counts the requests, the client JSON-RPC messages, the messages blocked (answered with a shield error, or responses replaced), the messages with at least one injection or credential detection, and the responses that passed uninspected (too large or still compressed). Per tool, it counts calls, blocks and detections, for up to 50 tools per endpoint; the rest are added up under *. Data is kept for 30 days.