Skip to content

Python API and MCP

GiulioDER edited this page Jul 23, 2026 · 2 revisions

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.


Part 1 — The Python API

The one call you need

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.

Branching on the result

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.text

Three 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.

Optional stages

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

Choosing a connection mode

PgVectorStore(DSN, dim=emb.dim)                 # one long-lived connection — CLI, scripts
PgVectorStore(DSN, dim=emb.dim, pool_size=8)    # pool — any server process

This 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.

Indexing from code

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.

Things worth knowing about the store

  • 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 a BYPASSRLS role, RLS is bypassed and the second isolation layer is decoration. → Tenancy-and-Auth
  • The store is a context manager. Use with, or call close().
  • A store is bound to one tenant for its lifetime. To serve several, hold several stores.

Part 2 — The MCP server

{ "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.

The four tools

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.

Two design details worth copying

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.

Transports

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.

Operational notes

  • 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_stats and 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.

Clone this wiki locally