Docs

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).

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"]}}
{
  "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:

{
  "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.)

The branch, in one table

the door saysit meansyour move
envelope with rows, isError: falsehere is the answeruse it
envelope with zero rows, isError: falsenothing matches — truthfullyaccept the answer; do not retry
problem document, isError: truerefused, for the named typed reasonswitch on type; the remediation is in the taxonomy
paginated result with structuredContent.nextPageTokenmore rows existpass 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.

We answer in writing. We take at most five conversations a month, only when you ask for one, and only after you already have the written read.