Lorekeeper exposes 10 MCP tools over stdio. All tools follow the MCP protocol — any MCP-compatible agent can use them.
| Tool | Purpose |
|---|---|
lore_search |
Hybrid semantic + keyword search with relevance scores |
lore_remember |
Fast one-shot memory save (auto-titles, auto-links) |
lore_insert |
Bulk structured insert with custom scores and links |
lore_update |
Feedback loop — rate memories, drive quality |
lore_forget |
Soft-delete wrong or outdated memories |
lore_reflect |
End-of-session: extract learnings, auto-save discoveries |
lore_processed_sessions |
Check which sessions are already processed |
lore_recommend_links |
Suggest candidate links between related memories |
lore_get_suggestions |
List pending link suggestions from the sweep engine |
lore_review_suggestion |
Accept or reject one or more link suggestions (batch) |
{
"query": "checkout payment flow",
"min_score": 0.1,
"include_links": true,
"include_deleted": false,
"refine_from": null,
"format": "full",
"ids": null,
"created_after": null,
"updated_after": null,
"sort_by": "relevance"
}Returns ranked memories with relevance scores and linked memories.
Query mode (default) — runs the hybrid semantic + BM25 pipeline:
query(required unlessidsis set): search textmin_score(default0.1): minimumcombined_scorethreshold — applied to the combined score before time-decayrefine_from: list oflore_idstrings from a previous result to re-rank only within that candidate set. Unknown IDs silently ignored. Max 200 (configurable viaLORE_MAX_REFINE_FROM_IDS)format:"full"(default) returns complete memory objects with relevance scores;"title"returns compact{id, title, score}for lower token costlimit: max results (defaults toLORE_SEARCH_LIMIT)include_links(defaulttrue): forced off informat='title'modeinclude_deleted(defaultfalse)created_after(optional): ISO 8601 UTC timestamp — only return memories withcreated_at ≥ T. Naive strings treated as UTC. Non-UTC offsets raise an error. Composes with all other filters.updated_after(optional): ISO 8601 UTC timestamp — only return memories withupdated_at ≥ T. Same UTC rules ascreated_after.sort_by(default"relevance"): controls result ordering."relevance"ranks by hybrid score when the search pipeline runs; in ids lookup mode (whenidsis set) there is no scoring, so"relevance"preserves the caller-providedidsorder instead."recent"sorts byupdated_at DESC."frequent"sorts byusage_count DESC. Composes with timestamp filters andlimit.
ID lookup mode — when ids is set, skips the vector/BM25 pipeline entirely and fetches those specific lore_ids directly from SQL. query is ignored. Unknown IDs silently ignored. Max 50 IDs (configurable via LORE_MAX_SEARCH_IDS). Pair with format='title' for a two-step list-then-fetch workflow. created_after, updated_after, and sort_by all compose with the ids path.
combined = 0.45·semantic + 0.30·keyword + 0.15·(score/10) + 0.10·log_usage_norm
final = combined × decay
Where:
semantic— cosine similarity from the vector store (LanceDB), normalised to [0, 1]keyword— BM25 score, top-hit normalised to 1.0score/10— the memory's quality score (0–10), normalisedlog_usage_norm = log2(1 + usage_count) / log2(1 + cap)— usage frequency signaldecay = e^(-λ · days_since_last_used)— time-decay so stale memories rank lower.λdefaults to0.0077(~90-day half-life), configurable viaLORE_DECAY_LAMBDA. Set to0to disable.last_usedupdates on every retrieval; falls back tocreated_atif never retrieved
All weights are env-configurable via LORE_W_* vars (see config.py).
Semantic candidates: top 200 from Mem0. Keyword candidates: BM25. Both pools are unioned then re-ranked.
{
"memories": [
{
"title": "Mutable default args in Python",
"description": "def f(x=[]) shares the list across all calls — use None instead.",
"content": "..."
},
{
"title": "Token refresh interval",
"content": "Access tokens expire after 1h.",
"links": [
{
"target_memory_id": "<target-uuid>",
"relation_type": "references",
"reason": "part of OAuth flow"
}
]
}
],
"links": [
{
"source_memory_id": "<uuid>",
"target_memory_id": "<uuid>",
"relation_type": "references",
"reason": "Both about Python gotchas"
}
],
"force": false
}Each memory dict:
| Field | Required | Default | Description |
|---|---|---|---|
title |
✅ | — | Short unique label for the memory |
content |
— | — | Full text to store |
description |
— | — | Brief summary |
score |
— | 5.0 |
Initial quality score (0–10) |
links |
— | — | Inline links to create after insert (see link schema) |
Top-level links (linking existing memories to each other) and per-memory inline links can be combined in one call.
Before inserting, a dedup score is computed: 0.6·semantic + 0.4·keyword. If it meets or exceeds LORE_DUPLICATE_THRESHOLD (default 0.85), the insert is blocked and the existing memory is returned. Set force: true to bypass.
Note: force=true bypasses the semantic/keyword dedup check, but the DB still enforces UNIQUE(namespace, title) — inserting two memories with the same title in the same namespace will surface a constraint error in errors[].
{
"thought": "Hybrid search formula: 0.45 semantic + 0.30 keyword + 0.15 score + 0.10 usage"
}Fast one-shot memory insert. Pass a single string; the server:
- Auto-extracts the title (first ~80 chars, sentence boundary)
- Stores the full content verbatim with default score
7.0 - Auto-links to the nearest semantic neighbor if similarity ≥
0.75
Returns {id, title, linked_to: {id, score} | null}.
Uses the same dedup pipeline as lore_insert — exact title matches are definitive duplicates.
Use lore_remember for quick capture. Use lore_insert when you need explicit titles, descriptions, scores, or manual links.
{
"memory_feedback": [{ "id": "<uuid>", "useful": true, "confidence": 8 }],
"link_feedback": [{ "id": "<uuid>", "useful": false, "confidence": 3 }]
}Drives the quality signal loop. Call after every lore_search to keep scores calibrated.
Each feedback call:
useful=true→ score bumped up byLORE_SCORE_BUMP_UP × (confidence/10)useful=false→ score bumped down byLORE_SCORE_BUMP_DOWN × ((11-confidence)/10)useful=false+confidence ≤ 2→ memory is soft-deleted (never returned in future searches)
Confidence is stored as a running EMA over the last 20 ratings (LORE_CONFIDENCE_WINDOW_SIZE).
Unknown memory IDs are returned in errors[] — they do not raise exceptions.
{
"memory_ids": ["uuid1", "uuid2"],
"reason": "hallucinated"
}Immediately soft-deletes one or more memories. The memories are excluded from lore_search results after this call.
reason must be one of: duplicate, hallucinated, outdated, expired, unspecified. Logged for auditability.
Soft-delete is reversible at the DB level, but no undelete tool is exposed via MCP.
Returns:
{ "forgotten": [...], "not_found": [...], "errors": [...] }{
"session_id": "uuid1",
"summary": "Implemented reflect integration; extracted 3 lessons.",
"session_date": "2026-05-19",
"topic": "reflect-integration",
"task_type": "build",
"what_was_done": "Built the reflect integration...",
"decisions": "- Used single-session submit for context efficiency",
"lessons_learnt": ["Don't skip dedup check before inserting"],
"good_patterns": ["Parallelise independent API calls"],
"factual_discoveries": ["BM25 rebuild costs ~10ms at 5k memories"],
"memory_ids": ["uuid-a", "uuid-b"],
"auto_insert": true
}Marks one session as processed and stores its content in the dashboard Sessions tab. Call once per session.
Auto-insert (default auto_insert=true): Each item in factual_discoveries and lessons_learnt is automatically inserted as a standalone searchable memory:
factual_discoveries→ score7.0, linked with"discovered_in"relationlessons_learnt→ score8.0, linked with"learned_in"relation
Duplicate-guarded: items already in the store return "status": "duplicate" with the existing ID; new inserts return "status": "inserted". Pass auto_insert=false to store only in the reflection record without creating memories.
Idempotency: If session_id was already processed, returns immediately with "already_processed": true and "memories_created": []. Check already_processed to detect retries.
Returns:
{
"reflection_id": "...",
"session_id": "...",
"created_at": "...",
"memories_created": [
{
"id": "m-1",
"title": "BM25 rebuild costs ~10ms...",
"relation": "discovered_in",
"status": "inserted"
},
{
"id": "m-2",
"title": "Don't skip dedup check...",
"relation": "learned_in",
"status": "inserted"
}
]
}No parameters.
{}Returns all session IDs already marked as processed by lore_reflect. Use this to avoid re-processing sessions.
Returns:
{ "session_ids": ["session-uuid-1", "session-uuid-2"] }{
"lore_id": "<uuid>",
"top_k": 10
}Suggests link candidates between a memory and related memories. Does not write any links — returns candidates for review. Confirm by calling lore_insert with links: [...].
Parameters:
lore_id(required): source memory to find candidates fortop_k(optional, default fromLORE_LINK_TOP_M): max candidates to return
Scoring pipeline — semantic cosine, BM25, entity overlap, and temporal proximity combined into a per-signal weighted score.
Returns:
{
"candidates": [
{
"source_lore_id": "<uuid>",
"target_lore_id": "<uuid>",
"weighted_score": 0.65,
"scores": {
"cosine": 0.82,
"bm25": 0.45,
"entity": 0.0,
"temporal": 0.0
}
}
],
"count": 10,
"source_lore_id": "<uuid>"
}Per-signal scores let the agent make its own judgment about which candidates are worth linking — no LLM call inside Lorekeeper.
List pending link suggestions generated by the background sweep engine.
{
"limit": 20,
"min_score": 0.0
}| Parameter | Type | Default | Description |
|---|---|---|---|
limit |
int |
20 |
Max suggestions to return (capped at 100) |
min_score |
float |
0.0 |
Minimum weighted_score filter (0.0-1.0) |
Response:
{
"suggestions": [
{
"id": "uuid",
"source_memory_id": "uuid",
"target_memory_id": "uuid",
"source_title": "...",
"target_title": "...",
"weighted_score": 0.87,
"suggested_type": "references",
"confidence": "high",
"status": "pending"
}
],
"count": 1,
"total_pending": 5
}total_pending gives agents a sense of workload without a separate count call. Only status='pending' suggestions are returned; min_score filters within that set by weighted_score.
Accept or reject one or more link suggestions in a single call. Each item is processed independently — a failure on one does not abort the rest.
{
"suggestion_ids": ["uuid1", "uuid2"],
"action": "accept"
}| Parameter | Type | Description |
|---|---|---|
suggestion_ids |
list[str] |
One or more suggestion UUIDs |
action |
str |
"accept" or "reject" |
On accept: Creates a real memory_links row using the suggestion's suggested_type (falls back to 'references' if None or unrecognised). The suggestion is retained with status='accepted' for audit trail.
On reject: Sets status='rejected' on the suggestion row. Future sweeps skip this pair.
Idempotent: Double-accept and double-reject both return status='skipped' — no error.
Response:
{
"results": [
{
"id": "uuid",
"status": "accepted",
"link_id": "uuid",
"message": "Link created"
}
],
"accepted": 1,
"rejected": 0,
"skipped": 0,
"errors": []
}status per item is one of "accepted", "rejected", "skipped", or "error". link_id is the created link ID on accept, null on reject, skip, or error.
When status="error", the item is also present in errors[] with the exception message. Callers iterating results[*].status should treat "error" as a failure sentinel and cross-reference errors[] for details.
All env vars use the LORE_ prefix. Key configuration:
| Variable | Default | Purpose |
|---|---|---|
LORE_DATA_DIR |
~/.lorekeeper |
Where SQLite + vector DB live |
LORE_SEARCH_LIMIT |
10 |
Default max results from lore_search |
LORE_DUPLICATE_THRESHOLD |
0.85 |
Dedup block threshold (0.6·semantic + 0.4·keyword) |
LORE_DECAY_LAMBDA |
0.0077 |
Time-decay rate (~90-day half-life). Set 0 to disable |
LORE_W_SEMANTIC |
0.45 |
Hybrid ranking: semantic weight |
LORE_W_KEYWORD |
0.30 |
Hybrid ranking: BM25 weight |
LORE_W_SCORE |
0.15 |
Hybrid ranking: quality score weight |
LORE_W_USAGE |
0.10 |
Hybrid ranking: usage frequency weight |
LORE_SCORE_BUMP_UP |
0.5 |
Score delta for useful=true feedback |
LORE_SCORE_BUMP_DOWN |
0.5 |
Score delta for useful=false feedback |
LORE_CONFIDENCE_WINDOW_SIZE |
20 |
EMA window for running confidence score |
LORE_MAX_REFINE_FROM_IDS |
200 |
Max IDs accepted in refine_from |
LORE_MAX_SEARCH_IDS |
50 |
Max IDs accepted in ids (lookup mode) |
LORE_LINK_TOP_M |
10 |
Default max candidates from lore_recommend_links |
LORE_NAMESPACE |
shared |
Default namespace for writes |
Full list with types and defaults → src/lorekeeper/config.py.