# Errors and events

> Every JSON-RPC error code and HTTP status MCP Shield returns, why, and the event each one leaves.

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:

```json
{"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:

```sh
openwaf-cloud ctl ai report example.com --view events --kind mcp_injection_response,mcp_dlp --limit 50
```

## Counters

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.
