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
- What capture stamps: who vs capturedBy.
- The tools' shared event model: the event.
- The door's own page, with the full tool table: /mcp/.
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.