---
name: haematite
description: Use typed Haematite tools to describe stores, query records, read history, and prepare and review branch changes.
---
Use the connected Haematite MCP server. Both Claude Code and Codex connect to `https://haematite.ablative.com.au/mcp` through the same stdio bridge. The Lys issuer is `https://lys.ablative.com.au` (the origin, without `/api`). No service on this computer is needed.
Install from the repository marketplace with `claude plugin marketplace add <repository>` then `claude plugin install haematite@haematite`, or `codex plugin marketplace add <repository>` then `codex plugin add haematite@haematite`. For Claude Code, set the plugin user config `lys_agent` to your Lys agent id and `key_file` to the absolute path of its PKCS#8 PEM or Lys seat JSON file. The `url` and `lys_issuer` options default to the central addresses above. For Codex, set `HAEMATITE_LYS_AGENT` and `HAEMATITE_KEY_FILE` for the plugin process. Both hosts use `HAEMATITE_URL=https://haematite.ablative.com.au/mcp` and `HAEMATITE_LYS_ISSUER=https://lys.ablative.com.au` unless explicitly overridden. `HAEMATITE_BIN` or `haematite` on PATH selects the binary.
The launcher runs `haematite mcp --remote <url> --lys-agent <issuer origin> <agent id> --key-file <absolute path>`. A credential file or a named run-pass variable can replace the key file. The pass stays in memory until its expiry, checked when a request arrives. HTTP 401 asks Lys once for a fresh pass and retries that request once. A second refusal preserves the server's code in the JSON-RPC error. No timer refreshes credentials. A loopback endpoint or issuer that does not answer a TCP connection refuses startup with exit 78 and `haematite_mcp_remote_unanswered` or `haematite_mcp_lys_unanswered`.
Local service mode requires an explicit `HAEMATITE_CONFIG` (Claude's optional `config_file`). Installed bundles also use central defaults; enrolment records the agent id and absolute key path without copying the key. Enrolled bundles have no `userConfig` prompt. Their bundled `serve.json` is a snapshot for explicit local use; later changes to `socket` or `data_dir` require a new bundle directory. Repeating installation at the same directory with changed inputs refuses `bundle_conflict`; use a new bundle directory.
Read the connected server's `tools/list` definitions before constructing arguments. Each tool's `inputSchema` and `outputSchema` is the contract; validate nested values against those definitions and preserve their scalar representation. Do not infer fields from a tool's purpose or manufacture a caller identity.
Describe the selected store before choosing a collection or relationship. Keep reads tied to the captured version. For `run_query`, pass output `rows.next_after` back as input `after` until a bounded read is complete. Report a partial answer as partial. All read tools take `after`; describe and review use a map of per-part cursors. Row limits are nonzero u32 values. `page.bytes` bounds the complete structured payload; the MCP envelope and duplicate text representation are outside that bound. Describe counts are complete values, with no cursor or partial wrapper. Preserve structured results and named refusals, without rounding large numbers.
Prepare changes on a branch and review their changes, conflicts and dangling endpoints before merging. `merge_branch` merges `from` into `branch` only at the `reviewed` version that `review_branch` read and only while `branch` is still at `expected`; any other version is refused, never substituted. Merge only after the review's conflicts and dangling findings are read to their end. Preserve request identifiers and expected versions. If a mutation reports an unknown outcome, stop mutations and report the refusal and identifier for reconciliation; do not retry it as a new request.
For `create_branch`, input `branch` names the source branch, and `expected` is its current head for the mutation guard. Input `name` names the new destination branch; `from` is the retained source version from which that destination forks. The receipt's `branch` names the destination, `version` is the destination's version, and `request_id` echoes the mutation identifier for reconciliation. The current source head and the retained fork version have separate roles.
read_history returns retained branch snapshots: branch, version and shard roots. Attribution is not stored by this service; history does not invent a caller, request identifier, change or timestamp.
review_branch returns one page for each part. Keep the captured selection version when continuing a part; pass its `next_after` back in `after` under that part's name. Native review begins at the current branch head. A requested version or target that differs between parts is refused by name, never substituted or automatically retried. Track completed parts separately and never add a repeated first page to a partial total.
The review counts page carries partial changes and conflicts. It makes no dangling count or safety claim. Read the dangling findings through their own cursor. Preserve each finding's state, missing_end, removed_by, relation, endpoints and choice_token; depends_on_choice does not mean a settled dangling endpoint. Differences and preparation lines use the same value form: `{"kind":"absent"}` for no value, or `{"kind":"value","value":<JSON>}` for a value, including JSON null. Preparation lines keep ancestor, target and source, plus their state and choice token. The page can include source_only and same_on_both as well as conflict.
## Network connection
For an explicitly chosen local development service, run the foreground listener with `haematite mcp-http --config /absolute/service.json`.
The existing service must already run at the configured absolute socket. Add an
explicit `mcp_http` object to that service configuration. Every field it accepts
is shown; any other field refuses startup:
```json
{
"listen": "127.0.0.1:19041",
"allowed_origins": ["https://agent.example"],
"path": "/mcp",
"resource_metadata_url": "https://identity.example/.well-known/oauth-protected-resource",
"max_connections": 64,
"header_timeout_ms": 2000,
"body_timeout_ms": 5000
}
```
`listen` must be a loopback address; expose it through a TLS proxy that
preserves Authorization and Origin. No proxy, certificate or service is
installed by this command. `path` is the one request path answered (`/mcp`
unless set), so two listeners behind one hostname can each own a path.
`allowed_origins` lists canonical origins without paths; an Origin, when
present, must be listed. `resource_metadata_url` is optional and is named in
the challenge of every 401; no metadata endpoint is served. The other values
shown are the defaults.
The listener serves POST on its path with JSON responses and accepts
notifications with HTTP 202. GET is refused with HTTP 405; it does not open an
event stream or mint sessions. Send Content-Type application/json, Accept
application/json and text/event-stream, and the negotiated MCP-Protocol-Version.
Who may come in is not a listener setting. Every request carries the caller's
own Lys pass for haematite as its bearer, and the listener presents that pass to
the store, which judges it and every act under it by that caller's grants. A
request with no bearer refuses 401 `mcp_sign_in_required`. A pass the store
refuses as missing, expired, not yet valid, for the wrong audience or issuer,
under an unpublished key, or badly signed or encoded refuses 401
`mcp_pass_refused`: ask Lys for a fresh pass. A store that cannot be reached,
or refuses for another reason, answers 503 `mcp_store_unavailable`.
Connections, including unfinished headers, are bounded by `max_connections`
(`mcp_http_connection_busy`); calls to the store are bounded by the service
file's `max_clients` (`mcp_http_busy`). The header and body deadlines are
independent, and response writes use the body deadline. The service file's
existing `timeout_ms` still bounds each native transport phase; a tool can make
several native operations, so these knobs do not claim a single total deadline
for a whole tool call.
Each answered request half-closes the write side after the complete answer, then
reads and discards whatever the peer still sends for up to 1 s before closing
(`mcp_http_linger_timeout` in the log when the peer outlasts it). This bounds the
connection slot held by a peer that leaves its write side open. Connection-cap,
socket and spawn refusals on the accept thread write, half-close and drop without
a linger read, so a peer's delayed close cannot hold that loop.
| Tool | Purpose |
| --- | --- |
| `describe_store` | Describe one captured store version in bounded pages. |
| `run_query` | Read one bounded page from a query. |
| `read_history` | Read retained branch snapshots or a record at a captured version. |
| `define_collection` | Define a collection and its shape on a branch. |
| `put_records` | Put one or more records on a branch atomically. |
| `delete_record` | Delete a record and its incident edges and vectors. |
| `link_records` | Link two records on a branch. |
| `unlink_records` | Remove a link between two records on a branch. |
| `create_branch` | Fork a branch from a retained version. |
| `review_branch` | Read bounded branch differences, preparation states and dangling findings. |
| `merge_branch` | Merge a reviewed branch, at the version its review read, into the branch it was forked from. |