# The refusal grammar

Every door on this surface answers a read in exactly one of three
shapes, and an agent needs exactly one branch per shape:

- **The result.** The data, in the standard's own envelope — an
  `EPCISQueryDocument` page for queries, a capture receipt for writes.
- **The truthful empty.** The same envelope with zero rows. Nothing
  matched, and that is the answer — status 200, `isError: false`,
  nothing to retry, nothing to apologize for. **A truthful empty is
  never an error.**
- **The typed refusal.** An RFC 7807 problem document carrying an
  `epcisException:*` type — the door knows why it cannot answer and
  says so in a machine-matchable field
  ([the taxonomy](/docs/errors/exceptions/)).

One law cuts across all three: **absence, not redaction.** An event
outside the caller's scope is indistinguishable from an event that
was never captured — queries omit it, `get_event` answers 404. No
answer shape discloses that something withheld exists.

## A truthful empty, run for real

A fresh stdio MCP session (`npx -y epcis.dev mcp`) holds a
session-local store with nothing in it; a well-formed query answers
with an empty page, not an error:

```
tools/call query {"query":{"EQ_bizStep":["shipping"]}}
```

```json
{
  "structuredContent": {
    "@context": [
      "https://ref.gs1.org/standards/epcis/epcis-context.jsonld",
      { "spine": "https://epcis.dev/ns/spine/v1" }
    ],
    "type": "EPCISQueryDocument",
    "schemaVersion": "2.0",
    "epcisBody": {
      "queryResults": {
        "queryName": "SimpleEventQuery",
        "resultsBody": { "eventList": [] }
      }
    }
  },
  "isError": false
}
```

`eventList: []` with `isError: false` — a complete, honest answer. An
agent that treats this as a failure and retries is fighting the
grammar. (`creationDate` and the `content` text block elided for
width; every remaining byte is verbatim from the run.)

## The MCP mirror: a refusal is a result

MCP transports carry refusals as **tool results with
`isError: true`**, never as JSON-RPC protocol errors — the JSON-RPC
layer succeeded; the door declined. The tool result body is the same
problem document the HTTP doors send, so one matcher serves both
transports. Cut live from `POST https://epcis.dev/mcp`, a keyed tool
reached with no key:

```json
{
  "jsonrpc": "2.0", "id": 1,
  "result": {
    "structuredContent": {
      "type": "epcisException:SecurityException",
      "title": "Unauthorised request",
      "status": 401
    },
    "isError": true
  }
}
```

(The `detail` field — which states the exact ceremony: send
`Authorization: Bearer` with a capture-scoped key — is shown in full
on [the typed exceptions](/docs/errors/exceptions/).)

## The branch, in one table

| the door says | it means | your move |
|---|---|---|
| envelope with rows, `isError: false` | here is the answer | use it |
| envelope with zero rows, `isError: false` | nothing matches — truthfully | accept the answer; do not retry |
| problem document, `isError: true` | refused, for the named typed reason | switch on `type`; the remediation is in the taxonomy |
| paginated result with `structuredContent.nextPageToken` | more rows exist | pass the token back; absence of the token is the last page |

Unknown tool names are the one exception that stays at the protocol
layer: calling a tool the server never advertised is a JSON-RPC
`-32602` error, because there is no door to decline.

---

**Proof, not adjectives.** This build could not verify the gateway's test suite green, so no sentence on this page claims a passing run — the last verified run is recorded in the repository's own CI, not here. Live on this origin, no key: POST /translate, /validate, /hash. No conformance attestation has ever been issued. The dated ledger is /what-ships-today/.
