Docs

MCP in one paste

One job: wire a client, and know before the first call which tools answer keyless and which answer a typed refusal. Two transports, one engine.

Local: stdio, session-local store

{ "mcpServers": { "epcis": { "command": "npx", "args": ["-y", "epcis.dev", "mcp"] } } }

The stdio server advertises four tools — capture, query, get_event, translate — over newline-delimited JSON-RPC 2.0. stdout carries only the MCP wire; diagnostics go to stderr.

The store is session-lived: capture events, then query or trace them back in the same process. Everything runs on your bench; nothing leaves it. A door refusal — rejected capture, out-of-scope read, bad query — is a tool result with isError: true mirroring the gateway's problem document; it is not a JSON-RPC error. Paginated tools lift the continuation into structuredContent.nextPageToken; its absence is the last page.

Hosted: POST /mcp on this origin

The same protocol answers at POST /mcp — JSON-RPC 2.0 over HTTP. six tools, each with the annotation a client's auto-approval logic reads. Executed against the live door 2026-08-05, pasted:

$ curl -sS -X POST https://epcis.dev/mcp -H 'content-type: application/json' \
    -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
  | jq -r '.result.tools[] | "\(.name)\t\(.annotations)"'

capture     {"readOnlyHint":false}
query       {"readOnlyHint":true}
get_event   {"readOnlyHint":true}
trace       {"readOnlyHint":true}
translate   {"readOnlyHint":true,"idempotentHint":true}
resolve     {"readOnlyHint":true}
subscribe   {"readOnlyHint":false}

initialize, tools/list, ping and the translate tool answer with no key. resolve and subscribe answer a typed RFC 7807 refusal (epcisException:ImplementationException 501, isError: true), never a fabricated result — identity resolution lives at id.org.ai (Agent. Human. Thing.), and no surface here describes its mechanics. The refusal body is on the typed exceptions.

The keyed tools, and the refusal that is the contract

capture, query, get_event and trace open with a capture key. Without one, the answer is the contract — executed 2026-07-31:

$ curl -sS -X POST https://epcis.dev/mcp -H 'content-type: application/json' \
    -d '{"jsonrpc":"2.0","id":3,"method":"tools/call",
         "params":{"name":"capture","arguments":{"document":{"type":"EPCISDocument"}}}}'
{
  "result": {
    "structuredContent": {
      "type": "epcisException:SecurityException",
      "title": "Unauthorised request",
      "status": 401,
      "detail": "/capture is a keyed door: send Authorization: Bearer <capture-scoped key>. Ask for one at /get-a-key/; we email you when it is ready."
    },
    "isError": true
  }
}

Branch on isError and the problem type, never on the detail prose. The refusal is typed precisely so an agent can plan around it: run keyless against the stdio door today, and say what you will capture at /get-a-key/ for the hosted write half.

Where next