Skip to content

Python API and MCP

giulio d'erme edited this page Aug 10, 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.generation_store import GenerationStore
from recall.embeddings import FastEmbedEmbedder
from recall.trust import trusted_search

emb = FastEmbedEmbedder()
with GenerationStore(DSN, dim=emb.dim, tenant="acme", pool_size=8) as store:
    store.check_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.

For local experiments against a plain chunk table, PgVectorStore remains available. In production, use generation-routed serving so the query sees an active generation, lineage and certified calibration binding rather than an ad hoc table.

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). MCP responses also include tenant, generation, calibration identity, retrieval profile and latency metadata so an operator can tell which process profile answered.

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,                      # legacy local table mode only
    reranker=CrossEncoderReranker(),      # needs the `rerank` extra
    entailment=QnliEntailmentJudge(),     # needs the `entail` extra — read the caveat below
)

Reranking helps when a relevant document is in the candidate pool but ranked too low. It is the largest single retrieval gain measured here, and it remains opt-in because it is a latency and model-artifact cost. → 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 and 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

  • check_schema() is the runtime call. Apply ordered migrations separately with recall schema apply, using RECALL_MIGRATION_DSN.
  • ensure_schema() is deprecated compatibility for disposable local stores and evaluation harnesses.
  • 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_SERVING_DSN": "postgresql://...", "RECALL_TENANT": "acme" } } } }

Works with Claude Code and Claude Desktop — the same block. Full setup guide: docs/USING_WITH_CLAUDE.md.

The five 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_evidence recall:read Builds a citable evidence bundle from trusted hits and returns generator-neutral system/user messages. The client remains the generator.
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.

recall_evidence stops before generation. It returns only trusted passages plus the prompt messages needed by a caller-owned generator, and an abstained retrieval produces an empty bundle. Validation checks citation structure only: every cited chunk ID must come from the supplied evidence.

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 or OIDC claim

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 configured authentication 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 static tokens, OIDC, 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.
  • Strict trust refuses unreadiness. In production, a missing generation, lineage mismatch, missing or uncertified calibration, stale calibration, or unavailable dependency is a refusal, not an empty result. Development mode can return degraded results for local work, but those hits are unverified context rather than a trust-layer claim.

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