# MCP tools

One tool surface, two doors. Local: `npx -y epcis.dev mcp` — stdio,
newline-delimited JSON-RPC 2.0, a session-lived store. Hosted: `POST
https://epcis.dev/mcp` — the same JSON-RPC envelope on the apex, where
`translate` answers with no key and the record-touching tools are keyed.

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

## The tools

| tool | does | on stdio (local) | on the apex door |
|---|---|---|---|
| `capture` | the one capture pipeline: strip → validate → stamp → append | answers; store is session-lived | keyed — without a capture key, the typed `epcisException:SecurityException` 401 as a tool result |
| `query` | one-shot SimpleEventQuery; Scope-filtered — out-of-scope events are absent, not redacted | answers | keyed, same 401 contract |
| `get_event` | one stored event by eventID as an EPCISQueryDocument page; out-of-scope is absent (404), never redacted | answers | keyed, same 401 contract |
| `translate` | EPCIS 1.1/1.2/2.0 XML → 2.0 JSON-LD + fidelity report | answers | answers with no key |
| `resolve` | identity resolution is homed at id.org.ai | — | answers a typed RFC 7807 refusal (`epcisException:ImplementationException` 501, `isError: true`), never a fabricated resolution |
| `subscribe` | standing subscriptions | — | answers the same typed 501 refusal — this deployment serves pull queries only; use `query` |

Calls to any name outside the advertised list are a `-32602` unknown-tool
error. Paginated tools lift the continuation into
`structuredContent.nextPageToken`; pass it back to page — absence means
last page.

### One tool is publish-gated, and the two doors disagree about its name

**Publish-gated:** the EPC walk, renamed `trace_epc` → `trace`.

It answers on **both** doors and is advertised under **two different names**,
because this repository renamed it and the published tarball predates the
rename. Measured 2026-08-05:

| door | name it advertises |
|---|---|
| `npx -y epcis.dev@0.1.0 mcp` (stdio) | renamed — still `trace_epc` |
| `POST https://epcis.dev/mcp` (apex, built from this repository) | `trace` |

It does the same thing at both: every in-scope event referencing an EPC in any
role (`MATCH_anyEPC`), paginated, keyed on the apex with the same 401 contract
as `query`. `docs/specs/verbs/epcis.dev.md` §A2 renames it and rules out an
alias; the tarball that carries the new name has not been published.

**So its catalog row is tier T2 and it is absent from the table above** — the
claims floor is the published tarball, and a name half the surface does not
serve cannot be a T1 claim. Bind the old name `trace_epc` → over stdio, and
`trace` on the apex, until one publish closes the gap.

## isError semantics — switch on it

A door refusal (rejected capture, out-of-scope read, bad query, missing
key) is a **tool result with `isError: true`** mirroring the gateway's
RFC 7807 problem document. It is not a JSON-RPC error. The keyed-tool
refusal on the apex, executed:

```json
{
  "jsonrpc": "2.0", "id": 3,
  "result": {
    "content": [{ "type": "text", "text": "{ …problem document… }" }],
    "structuredContent": {
      "type": "epcisException:SecurityException",
      "title": "Unauthorised request",
      "status": 401
    },
    "isError": true
  }
}
```

Match on `structuredContent.type` — the same `epcisException:*` taxonomy
every HTTP door uses. The refusal tools speak it too — `resolve`, cut
live from the apex door on 2026-07-31:

```json
{
  "structuredContent": {
    "type": "epcisException:ImplementationException",
    "title": "resolve is not served on this door",
    "status": 501,
    "detail": "identity resolution is homed at id.org.ai — one
      resolver, one home; this door answers a typed refusal,
      never a fabricated resolution."
  },
  "isError": true
}
```

(`detail` wrapped for width; every other byte verbatim.)

## Proof — initialize and tools/list

Over stdio against `epcis.dev@0.2.0`:

```
$ printf '%s\n%s\n' \
    '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
      "protocolVersion":"2025-06-18","capabilities":{},
      "clientInfo":{"name":"docs-proof","version":"0"}}}' \
    '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
  | npx -y epcis.dev mcp
```

The `initialize` result, verbatim, cut 2026-08-05 from a clean install of
the `0.2.0` tarball:

```json
{ "serverInfo": { "name": "epcis.dev-spine",
    "title": "epcis.dev event spine", "version": "epcis.dev/0.2.0" },
  "protocolVersion": "2025-06-18" }
```

Every tool documented on this page is advertised by `tools/list`, on the
door named beside it:

```
tools/list (stdio): capture, query, get_event, trace_epc, translate  # renamed
tools/list (apex POST /mcp): capture, query, get_event, trace,
                             translate, resolve, subscribe
```

Both lists were **re-cut from real runs on 2026-08-05** — the stdio door from
`npx -y epcis.dev@0.1.0 mcp`, the apex list from `POST https://epcis.dev/mcp`.
The two extra apex names are the honest-refusal tools in the table above:
advertised because calling them answers a typed error, never a guess. The
`trace_epc` → `trace` difference is the publish gate described above, not a
transcription slip — this page previously printed `trace` on both lines and
that was wrong about the tarball.

The store behind the record-touching stdio tools — `capture`, `query`,
`get_event` and the renamed `trace_epc` → does not survive the process. Batch
your capture-then-query work inside one MCP session.

---

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