-
Notifications
You must be signed in to change notification settings - Fork 0
Python API and MCP
Two ways to call RE-call: as a library, or as an MCP server an agent can invoke as a tool.
from recall.store import PgVectorStore
from recall.embeddings import FastEmbedEmbedder
from recall.trust import trusted_search
emb = FastEmbedEmbedder()
with PgVectorStore(DSN, dim=emb.dim, tenant="acme", pool_size=8) as store:
store.ensure_schema()
result = trusted_search(store, emb, "what is the rate limit?")trusted_search is the recommended agent-facing entry point: it runs hybrid retrieval, loads the
supersession graph, applies the trust layer, and returns a judged result. The lower-level pieces
(HybridRetriever, trust.evaluate) are public and composable, but you should not need them
unless you are building something unusual.
Check abstained first. Always.
if result.abstained:
# Say you don't know. Do NOT answer from result.hits — none of them earned a clean verdict.
log.info("no trustworthy memory: %s", result.reason)
return None
for hit in result.hits:
hit.verdict # ok | superseded | expired | not_yet_valid | low_confidence | ...
hit.confidence # calibrated; 0.5 sits exactly on the abstention boundary
hit.cosine # raw dense cosine
hit.validity.superseded_by # the successor, when superseded
hit.validity.valid_until
hit.provenance.file # where it came from
hit.provenance.indexed_at # when it entered the index
hit.chunk.textThree things that are easy to get wrong:
hits is not filtered. Verdict-ok hits are ordered first, and the rest follow. That
ordering is the answer — a retrieved successor outranking the stale memory it replaced is the
whole thesis — but if you iterate the list without checking verdict, you will happily read a
superseded memory in position two. Filter on verdict == "ok", or stop at the first non-ok.
abstained is not the same as an empty result. It means no hit earned verdict ok. There
may still be plenty of hits — superseded ones, expired ones, near-misses — and reason tells you
which case you are in. That is deliberate: an agent that can see why it is abstaining can act
differently on "the answer exists but was superseded and I could not retrieve the successor" than
on "this corpus has nothing about penguins".
calibrated=False is a real signal. It means no usable calibration file was found for this
embedder, so the abstention boundary is a default rather than fitted. Worth logging; worth
alerting on in production.
Two further flags: gap_warning (every dense candidate scored below the threshold — a probable
corpus gap) and staleness (the index itself has not been updated recently, which no individual
hit would reveal).
Full semantics of every field: The-Trust-Layer.
from recall.rerank import CrossEncoderReranker
from recall.entailment import QnliEntailmentJudge
result = trusted_search(
store, emb, query,
calibration=cal, # from recall.calibration.load_for()
reranker=CrossEncoderReranker(), # needs the `rerank` extra
entailment=QnliEntailmentJudge(), # needs the `entail` extra — read the caveat below
)Reranking helps where the embedder has not already saturated the corpus, and is redundant — and expensive — where it has. → Retrieval-Pipeline
Entailment is off by default for a measured reason: it substantially improves near-miss
detection and degrades far-gap detection. The two stages stack; neither replaces the other. It
costs one judge pass per ok hit. → The-Trust-Layer
PgVectorStore(DSN, dim=emb.dim) # one long-lived connection — CLI, scripts
PgVectorStore(DSN, dim=emb.dim, pool_size=8) # pool — any server processThis is not a performance dial, it is a correctness one. A single connection shared across
threads serialises them, and a reconnect can swap the connection underneath a thread that is
using it. Pooling requires the pool extra.
Also worth passing in a server: statement_timeout_ms, so a runaway query cannot occupy a
connection until the process dies.
from recall.index import Indexer
stats = Indexer(store, emb).index_path("./notes")Incremental by content hash, prunes sources that vanished from disk, and refuses a mass prune — see Retrieval-Pipeline for what each guard is protecting against.
-
ensure_schema()is idempotent and migrates a pre-tenancy table in place. Call it on open. -
check_rls_effective()tells you whether row-level security is actually enforced for the role you connected as. If you connect as a superuser or aBYPASSRLSrole, RLS is bypassed and the second isolation layer is decoration. → Tenancy-and-Auth -
The store is a context manager. Use
with, or callclose(). - A store is bound to one tenant for its lifetime. To serve several, hold several stores.
{ "mcpServers": { "recall": {
"command": "python", "args": ["-m", "recall_mcp.server"],
"env": { "RECALL_DSN": "postgresql://...", "RECALL_TENANT": "acme" } } } }Works with Claude Code and Claude Desktop — the same block. Full setup guide:
docs/USING_WITH_CLAUDE.md.
| Tool | Scope required | Does |
|---|---|---|
recall_search |
recall:read |
Search memory. Returns hits with verdict, confidence, provenance and validity — or an abstention. Also returns an advice field stating what to do. |
recall_index |
recall:write |
Index a file or folder into memory. Re-indexing a file replaces its chunks completely, so a shrunk file leaves nothing stale behind. |
recall_forget |
recall:forget |
Permanently delete a source's chunks. Irreversible, tenant-scoped. |
recall_stats |
recall:read |
Size and freshness of the memory. |
Each carries MCP annotations that describe its real risk — recall_forget is marked destructive,
the read tools are marked read-only — and the scopes mirror those annotations, so a
principal's permissions line up with the risk each tool actually carries rather than with an
arbitrary grouping.
recall_search returns advice. Not just data — an explicit statement of what the agent
should do. A model handed a JSON blob with abstained: true will often answer from the hits
anyway; a model handed "no valid hit survived, say you don't know" is much more likely to comply.
The tool docstring reinforces it: call this before proposing an idea, and if a closed decision
surfaces, do not re-litigate it. The anti-re-litigation guard is as much a prompt-design problem
as a retrieval one.
recall_forget reports sources_not_found separately. A source that does not exist is not
silently counted as removed. If you asked to forget three things and two existed, you need to know
that — otherwise an erasure request looks satisfied when a third of it silently did nothing. Check
that list before assuming a name was forgotten.
| Transport | Listener | Auth | Tenancy |
|---|---|---|---|
stdio (default) |
none — a private pipe to one client | not required | one tenant, from RECALL_TENANT
|
streamable-http, sse
|
TCP socket | required, enforced at startup | one tenant per token |
stdio needs no authentication because there is no remote caller: the client owns the process,
and the pipe is the boundary. Starting an HTTP transport without a token file raises and
refuses to boot — chosen over logging a warning, because a warning produces a server that comes
up looking healthy with every memory in it readable by anything that can reach the port.
→ Tenancy-and-Auth for tokens, scopes and the rate limits.
-
Tools are async and offloaded to threads. FastMCP awaits async tools and calls sync ones
inline — there is no thread pool doing it for you — so a synchronous tool body would serve
exactly one request at a time. Combined with
pool_size, this is what makes the server actually concurrent. - Rate limiting runs after authorisation, so an unauthorised caller cannot burn a tenant's budget by hammering a scope it does not hold.
- The indexing budget is debited pre-flight — the tenant is charged for what is about to be embedded, before it is embedded, so a refusal has spent nothing.
- Logs go to stderr with propagation off. stdout carries JSON-RPC; a stray log line there would corrupt the protocol.
-
Metrics for abstention, verdicts, reconnects and latency percentiles are surfaced through
recall_statsand the metrics registry. The library never attaches logging handlers itself — that is the host's job.
See also: The-Trust-Layer for what the verdicts mean · Configuration-Reference for every environment variable · CLI-Reference for the same operations from a shell.
This wiki explains design and intent. Measured figures, deployment defaults and CLI flag values
live in the repository, versioned with the code that produced them:
FINDINGS ·
RESULTS ·
MIGRATIONS ·
.env.example · --help.
If a page here disagrees with the repo, the repo is right.
Concepts
Using it
- Installation-and-Setup
- Embedders-and-Rerankers
- Configuration-Reference
- CLI-Reference
- Python-API-and-MCP
- Tenancy-and-Auth
Evidence
Contributing
In the repo