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 five tools — executed 2026-07-31 against epcis.dev@0.1.0:

→ {"jsonrpc":"2.0","id":1,"method":"initialize","params":{…}}
← "serverInfo": { "name": "epcis.dev-spine", "version": "epcis.dev/0.1.0" }
→ {"jsonrpc":"2.0","id":2,"method":"tools/list"}
← ["capture","query","get_event","trace_epc","translate"]

The store is session-lived: capture events, then query or trace_epc 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, executed 2026-07-31:

$ curl -sS -X POST https://epcis.dev/mcp -H 'content-type: application/json' \
    -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
← tools: capture, query, get_event, trace_epc, translate, resolve, subscribe

initialize, tools/list, ping and the translate tool answer with no key. resolve and subscribe answer a typed tool-level error, never a fabricated result — identity resolution lives at id.org.ai (Agent. Human. Thing.), and no surface here describes its mechanics.

The keyed tools, and the refusal that is the contract

capture, query, get_event and trace_epc 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>. Keys are provisioned from the /get-a-key/ list, in order — one email when yours 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

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.