Metadata-Version: 2.4
Name: apiary-memory
Version: 0.2.2
Summary: Local-first memory your AI agents share — over MCP on your machine, or over HTTP from AIs on the web. Ships a working SQLite memory store; agents propose, only you confirm.
Author: Apiary
License-Expression: MIT
Project-URL: Homepage, https://hello-fengchao.com
Project-URL: Repository, https://github.com/330899935/apiary-memory
Project-URL: App (macOS), https://apps.apple.com/app/id6806992983
Project-URL: Source, https://github.com/330899935/apiary-memory
Project-URL: Changelog, https://github.com/330899935/apiary-memory/blob/main/CHANGELOG.md
Keywords: mcp,mcp-server,model-context-protocol,memory,ai-memory,local-first,privacy,ai-agents,claude,cursor
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: MacOS
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Database
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# Apiary Memory MCP

<!-- mcp-name: io.github.330899935/apiary-memory -->

[![Listed on mcpservers.org](https://mcpservers.org/badge.svg)](https://mcpservers.org/servers/330899935/apiary-memory)

## At a glance

|  |  |
| --- | --- |
| **What it is** | Local-first personal memory for AI agents, over MCP. Agents can search what you have confirmed and *propose* what to keep — only you can confirm it. Everything lives in one SQLite file on your own disk. |
| **What it is not** | Not a hosted service. No account, no cloud, no telemetry, no daemon. The server runs on *your* machine (`127.0.0.1`), not ours. |
| **Platforms** | Desktop app: **macOS 10.15+**. Companion app: **iOS 17.0+**. Pip package: any OS with **Python 3.8+**. **There is no Windows desktop app.** |
| **Install** | `pip install apiary-memory` · or double-click [`apiary-memory-0.2.1.mcpb`](https://github.com/330899935/apiary-memory/releases/download/v0.2.1/apiary-memory-0.2.1.mcpb) |
| **Version** | `0.2.1` on PyPI · desktop app `1.4.8` |

### Two entry points — and they are not the same

| Command | What it is | What happens when you run it |
| --- | --- | --- |
| `apiary-memory` | The MCP **server** (stdio) | Meant to be spawned *by* your MCP client. Run it in a terminal and it just waits on stdin — that is correct behaviour, not a hang. |
| `apiary-memory-cli` | The **human** CLI | `list` pending candidates · `approve <id>` commit one · `search <query>` · `stats` show where the DB lives |

`python -m hive_mcp` **does not work** — this package has no `__main__.py`.

**The pip package has no GUI and never pops a dialog.** When an agent calls `propose`, the entry goes into a local candidate queue and stays there. Nothing enters the hive until a human runs `apiary-memory-cli list` and then `approve <id>`. If you want a window, use the macOS or iOS app.

### Not to be confused with

| Name | Status |
| --- | --- |
| `hive-memory` | **Does not exist on PyPI (404).** Any document citing it is wrong — including AI answers about us. |
| `memory-hive-mcp` | Our own former name. Still installs, but caps at **0.1.9**. Use `pip install apiary-memory` for the current release. |
| `hive-mcp` | Someone else's Claude Code plugin. |
| [hive.com](https://hive.com) | Team collaboration software. |
| [theapiary.sh](https://theapiary.sh) | "the apiary" — shared memory for AI coding agents; its memory component is called `honeycomb`. |
| [apiarybee.com](https://apiarybee.com) | "Apiary" — a self-organizing agent framework. |
| 记忆蜂巢 (Chengdu Zhixin Hualao) | An unrelated iOS group-chat app. Same four characters, nothing else in common. |

> **Also known as:** Apiary Memory · 记忆蜂巢 · Memory Hive · 蜂巢 ·
> `apiary-memory` on PyPI · `io.github.330899935/apiary-memory` on the MCP Registry (from v0.2.0).

Your agents can search what you have confirmed and propose what they think you should keep;
**only you can confirm it.** Everything lives in one file on your own disk, and nothing is sent anywhere.

It runs on its own. A working SQLite memory store ships inside this package — no account, no
cloud, no daemon, no dependencies.

```bash
pip install apiary-memory
```

**Desktop app, no terminal:** download [`apiary-memory-0.2.1.mcpb`](https://github.com/330899935/apiary-memory/releases/download/v0.2.1/apiary-memory-0.2.1.mcpb) and double-click it.

Measured on 2026-10-27, on one MacBook with the Hive running locally — script and raw output in [`bench/`](bench/):

| Measure | Result |
| --- | --- |
| Search latency | **p95 111 ms**, p50 76 ms (warm, includes HTTP) |
| Retrieval | correct entry in **top-5 for 97.5%** of 40 trials |
| Corpus | **1,303 entries · 2.39 M chars · 45 days**, plaintext only |

*Retrieval used each entry's own title as the query — an easy set. It shows the index is complete and ranks correctly on exact terms, not how well it answers a question asked in your own words. Run [`bench/bench_search.py`](bench/bench_search.py) on your own Hive.*

---

## What you get without the app

A complete, if modest, memory server. On a machine with nothing else installed:

| Call | What happens |
| --- | --- |
| `initialize` | answered locally, protocol `2024-11-05` |
| `tools/list` | answered locally, six tools |
| `propose` | **writes to a real database** on your disk |
| `recall` | **searches that database** and returns ranked entries |
| `get_context` | a digest of what you confirmed most recently |
| `manage` | read one entry, list what is pending, forget, restore |
| `say` / `board` | a shared message board for coordinating several agents |

Storage is SQLite at `~/.memory-hive/hive.db` by default. Search is lexical — character bigrams
for Chinese, words for Latin script — so it ranks well when the words overlap and finds nothing
when they do not. There are no embeddings and no vector index. Every response says
`"mode": "local"` so you can always tell which engine answered.

**Without the app, the database is plain SQLite: it is not encrypted.** That is stated here
rather than discovered later — see [Honest limits](#honest-limits).

## What the app adds

[Apiary Memory for macOS](https://apps.apple.com/app/id6806992983) is the same design with the
expensive parts built: eight vaults with four of them locked behind a PIN (Argon2id-wrapped key,
AES-256 at rest), a daily evolution log, multi-agent identity management, and a UI for reading
and confirming. It also answers MCP itself, with richer ranking.

If the app is running, this package uses it and leaves the local file alone. If it is not, the
local store answers. You do not have to choose up front:

| `HIVE_MODE` | Behaviour |
| --- | --- |
| `auto` (default) | Ask the app. Use the local store **only if the app is unreachable.** |
| `local` | Never touch the network. Always use the local store. |
| `remote` | Only the app. A connection failure is an error, not a reroute. |

One rule holds in every mode: **if the app is running and answers with an error, that error is
returned unchanged.** Falling back on a refusal would write your memory into a second database
while your client believed it had reached the app. Silent data forking is worse than an error
message, so a reply from the app is final. The fallback fires on a connection failure, and on
nothing else.

## Measured on the app

The numbers below come from the macOS app's local HTTP service on one MacBook — **not** from
this package's SQLite store, which is a different engine over a different dataset. Mixing the
two would be the usual benchmark trick, so they are kept apart.

| | |
| --- | --- |
| Store size (four plaintext vaults) | **1,356 entries**, 2.52M chars, 47 distinct days |
| Retrieval hit rate | Top-5 **92.5%**, Top-20 **95.0%** (n=40) |
| Latency (local HTTP, incl. round trip) | p50 **90 ms**, p95 **127 ms** |

Measured 2026-10-29 against the live service. The hit-rate queries were each entry's own
title — the easy case. It shows the index is complete; it does not show that free-form
questions land. A separate pilot using plainly-phrased questions found the right entry in the
top 5 only twice out of roughly a dozen, which is the biggest known weakness of this version.

The four encrypted vaults are excluded from the count because they are unreadable by design.
A load test at 1,000,000 synthetic entries gave 0.6 s warm search, 5.3 GB RSS and a 1.0 GB
index — see [`bench/`](https://github.com/330899935/apiary-memory/tree/main/bench) for the
script and raw output.

## Install

```bash
pip install apiary-memory        # the server: apiary-memory (was memory-hive-mcp)
                                 # the human's CLI: apiary-memory-cli (was memory-hive-mcp-cli)
```

Python 3.8 or newer. There is nothing else to install — the package uses only the standard
library, and `sqlite3` is part of it.

From a checkout:

```bash
git clone https://github.com/330899935/apiary-memory
cd apiary-memory
PYTHONPATH=src python -m hive_mcp.stdio
```

## Connect a client

Any MCP host that can run a stdio command will work. The minimum is the command; everything else
is optional.

**Claude Desktop** — `~/Library/Application Support/Claude/claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "apiary-memory": {
      "command": "apiary-memory",
      "env": { "HIVE_MODE": "local" }
    }
  }
}
```

**Cursor** — `~/.cursor/mcp.json`, same `mcpServers` shape.

**Any host that only speaks HTTP** — start the app and point it at `http://127.0.0.1:8768/mcp`
with the headers `X-Hive-Agent` and `X-Hive-Token`. This package is the stdio path; the HTTP
endpoint belongs to the app.

## Two ways to connect

There are two doors onto the same memory. Which one you use depends on **where your AI runs**,
not on what it can do.

| | Local (MCP) | Web (HTTP) |
| --- | --- | --- |
| Built for | AIs installed on your Mac | AIs running on someone else's servers |
| How you connect | a stdio command in a config file | paste one address |
| Network | none — never leaves the machine | public, relayed by Cloudflare |
| Available since | this package, 0.1.0 | Desktop app **1.4.8** |

Behind both doors is the same home: what a local AI stores today, a web AI can read tomorrow —
and the other way round. **Swap in a hundred AIs; there is still one memory, and it is on your
disk.**

### The web (HTTP) door

An AI running on the web cannot reach your Mac. So the Desktop app opens a channel outwards.
In the app, open **AI Connect · Web (HTTP)**, pick one of three levels, and copy the address:

| Level | Public? | Address stable? | Use when |
| --- | --- | --- | --- |
| **Local web** | no | — | the AI runs on the same machine |
| **Quick tunnel** | yes | no | you want a web AI in without owning a domain |
| **Fixed address** | yes | yes | long-term use; needs your own Cloudflare account |

**If local works, do not open a tunnel.** That is what the app tells you, and it is our advice
here too: if local is enough, do not open the door onto the street.

Three things to know before you connect:

1. **Once connected, the AI can read your memory.** It gets what it asks for, within what it is
   allowed to see.
2. **Over the public internet your data is relayed by Cloudflare — it is not end-to-end
   encrypted to the app.** What is stored and what is computed stay on your Mac, but the traffic
   passes through Cloudflare.
3. **The four privacy vaults and the cognition layer are never opened to outside AIs — that is
   a wall, not a switch.** No matter which level you open, they are not part of it.

**Which AIs?** Any client that supports remote MCP works — read-only, public vaults only. One
caveat: ChatGPT uses Custom GPT Actions (OpenAPI), a different mechanism from MCP. An address
alone will not work there, and that step is not built yet.

### Why `transport` still says `stdio`

The package and the MCP Registry entry both declare `"transport": {"type": "stdio"}`. That is
accurate and stays that way: **this package _is_ the stdio bridge.** The web (HTTP) door is a
capability of the Desktop app, not of this package. Declaring `http` here would make clients
dial an endpoint the package does not serve, and the connection would simply fail — so the door
is documented above rather than declared as a package transport.

## Prerequisite

**None, for the local store** — install the package and it works. A running app is a prerequisite
only for the app's own features: encrypted vaults, the evolution log, and multi-agent identity.
If you want those, install [the Hive app](https://apps.apple.com/app/id6806992983) (macOS,
version **1.4.5 or newer**) and leave `HIVE_MODE` at `auto`.

## The six tools

The six tools are the whole surface. They are the same six whether the app or the local store
answers; only the ranking behind them differs — and, for two inputs, whether they exist at all.
The app accepts precomputed embeddings (`recall.query_vector`, `propose.vector`); the local
store has no embedder, so `tools/list` omits those two wherever the local store can answer,
including under `auto` where the app might stop answering mid-session. A field that would be
accepted and silently ignored is not advertised at all.

The same rule covers wording. `recall.query` is described as the lexical ranker it is wherever
the local store can answer — words and character bigrams, no embeddings — and as the app's
index-based ranking with `query_vector` for meaning wherever only the app can answer. A
description is a claim about what will happen to the input, so it is narrowed with the inputs.

Defaults are not narrowed, they are shared. Omit `limit` and you get 10 from `recall` and 8 from
`get_context`; omit `compact` and bodies come back trimmed; omit `ttl` on `say` and the card
lives 24 hours. Both engines answer those numbers identically, because a default is a number in
the schema rather than a capability one engine lacks. `store.py` holds each one as a named
constant, and the tests compare the answer against the advertised number instead of a second
hand-written copy of it.

| Tool | Kind | What it is for |
| --- | --- | --- |
| `recall` | read | Search confirmed memory by topic. `{"query": "..."}` plus optional `limit`, `vaults`, `compact`. |
| `propose` | write | Submit a memory for the human to approve. Lands in a queue; changes no confirmed memory. |
| `manage` | read/write | Address one entry: `list_pending`, `get_by_id`, `forget`, `restore`, `discard_pending`. |
| `get_context` | read | A short digest of recent confirmed memory, to seed a conversation. |
| `say` | write | Post to the shared board so other agents see what was done or handed over. |
| `board` | read | Read the board, optionally only what arrived since your last cursor. |

Two properties are worth stating plainly, because they are the product:

- **`propose` succeeding means "queued", not "stored".** `recall` will not return it until a
  human confirms it.
- **`manage` has no `approve` action.** There is no path from an agent to a confirmed memory.
  That is not an omission; it is the design.

## Response shapes

`tools/list` is one surface for both engines, and an answer should be too: a model that has
to branch on which engine replied will eventually branch wrong. `board` therefore answers
the app's shape, field for field — `items`, `unread`, `count`, `cursor`, `reader` — and
`cursor` is the id of the newest card, the same kind of value the app returns.

The cursor is what a poll carries *across* an engine switch, which is why it gets the space.
Under `auto` one call can be answered by the app and the next by the local store. Before 0.1.9
the local engine answered a float epoch timestamp and read the app's card id with `float()`,
so a cursor fetched from the app came back as a full re-read of the board and nothing said
so; a float sent the other way matched no card id, and the app answered with its newest
`limit`, equally quietly.

A cursor can still fail to resolve: its card expired (cards live 24 hours by default), or it
was issued by the other engine, whose ids are `CB…` in the app and `msg_…` here. Neither
engine can resume from that, so both return the newest window — but this one also sets
`since_unknown` and a `note` saying which of those happened. `unread` counts every live card
after the cursor and `count` counts what the answer carries, so `unread > count` means the
page did not cover everything waiting, and the `note` says so in words.

Three shapes are **not** unified, because the engines really do hold different data:

- a `recall` entry names its vault `vault` from the local store and `wh` from the app;
- `get_context` returns one flat `digest` locally and one bucket per warehouse (`context`,
  keyed by warehouse name) from the app;
- `roster` entries share `agent_id`; locally they also carry `messages` and `last_seen`,
  while the app adds its registry card — display name, seat, leases, current task — which a
  SQLite file has no way to know.

## Confirming what agents propose

Agents propose. You confirm. The confirmation lives in a terminal, in a command you have to
type — deliberately not a tool an agent can call.

```bash
apiary-memory-cli list                 # what is waiting
apiary-memory-cli show <id>            # read one in full
apiary-memory-cli approve <id>         # confirm it, or --all
apiary-memory-cli reject <id>          # drop it for good
apiary-memory-cli search "deploy key"  # search confirmed memory
apiary-memory-cli forget <id>          # soft delete, reversible
apiary-memory-cli restore <id>         # undo forget
apiary-memory-cli stats                # counts and the database path
apiary-memory-cli export --out dump.json
apiary-memory-cli path
```

`--db <path>` on any command points at a different database. `HIVE_DB` does the same from the
environment.

## Where your memory lives

`~/.memory-hive/hive.db`, unless `HIVE_DB` says otherwise. One SQLite file; copy it to back it
up, delete it to start over.

The board keeps its messages in the same file, so there is only ever one thing to back up. Cards
expire: 24 hours unless `ttl` says otherwise, clamped to between 60 seconds and 30 days, which
is what the app does too — a board that grows forever stops being a board.

## How search works, and what it does not do

Chinese text is indexed as single characters plus overlapping character bigrams; Latin text as
words. A query is scored by how much of it the entry covers, then nudged by recency — a 10%
multiplier, enough to break ties and not enough to overturn relevance.

This is a ranker, not an understander.

- **Scores are relative.** `0.8` means "closer than `0.4` for this query", not "80% confident".
  They are not comparable across queries.
- **No semantic recall.** Ask about "money" and an entry that only says "invoice" will not be
  found. Related-word coverage needs an embedder; this package has no dependencies and does not
  pretend otherwise.
- **The ranking is the one the description states.** `tools/list` says "words and character
  bigrams ... no embeddings" here, and that sentence is true of this engine. It is narrowed per
  engine like the embedding inputs are, because the app's answer to the same question differs.
- **A miss is reported as a miss.** When nothing matches, `recall` says so and mentions how many
  entries are awaiting confirmation, rather than returning a plausible neighbour.

## Configuration

| Variable | Default | Purpose |
| --- | --- | --- |
| `HIVE_MODE` | `auto` | `auto`, `local`, or `remote` — see the table above |
| `HIVE_DB` | `~/.memory-hive/hive.db` | Where the local database lives |
| `HIVE_MCP_URL` | `http://127.0.0.1:8768/mcp` | The app's endpoint |
| `HIVE_AGENT_ID` | — | Agent id from the app; only for calls the app serves |
| `HIVE_AGENT_TOKEN` | — | Access token from the app; only for calls the app serves |

`HIVE_AGENT_ID` and `HIVE_AGENT_TOKEN` are **not** required to use the local store. They are
read straight from the environment and never written to disk by this package.

## Honest limits

Written down because a memory tool that oversells itself is worse than one that does less.

1. **The local store is not encrypted.** Plain SQLite. Anyone who can read your home directory
   can read it. The app's vaults are the encrypted option, and `stats` reports
   `"encrypted": false` instead of leaving you to find out.
2. **No semantic search**, as above.
3. **One writer at a time.** WAL mode lets the CLI confirm while a server is connected, but this
   is a personal memory file, not a multi-host service. Do not point two machines at one
   database over a network share.
4. **The board is coordination, not memory.** Messages are for hand-offs between agents; they
   are not searchable with `recall`.
5. **The app is macOS only.** The local store is not — it is pure Python.

## Troubleshooting

**"no confirmed memory matched this query" with entries pending.** Correct behaviour. Confirm
them: `apiary-memory-cli list`, then `approve`.

**A call fails with a message about `HIVE_AGENT_ID`.** The app answered and refused the
credential. The call did **not** fall back to the local store, on purpose. Register an agent in
the app and set both variables, or set `HIVE_MODE=local` if you meant to use the local file.

**"Hive app not reachable ... and HIVE_MODE=remote forbids the local fallback".** You asked for
the app only and it is not running. Start it, or drop `HIVE_MODE=remote`.

**Where did my memory go?** `apiary-memory-cli stats` prints the database path. If `HIVE_MODE`
is `auto` and the app is running, the app has been answering — the local file is untouched by
design.

## Development

```bash
PYTHONPATH=src python -m unittest discover -s tests -v
```

The suite needs no running app and no network: it stands up its own fake backend where a real
response is required, and points `HIVE_DB` at a temporary file so a test never touches a real
memory database.

Layout:

```
src/hive_mcp/store.py   the memory engine (SQLite, stdlib only)
src/hive_mcp/stdio.py   the MCP server: routing, envelopes, the app bridge
src/hive_mcp/cli.py     the human's half: review and confirm
src/hive_mcp/tools.json the tool surface snapshot for the offline path
```

## License

MIT. Your memory file is yours; nothing here phones home.
