# 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

```json
{ "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](/docs/errors/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/](/get-a-key/) for the hosted
write half.

## Where next

- What `capture` stamps: [who vs capturedBy](/docs/event-model/who-capturedby/).
- The tools' shared event model: [the event](/docs/event-model/the-event/).
- The door's own page, with the full tool table: [/mcp/](/mcp/).

---

**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/.
