linklore
OfficialLinkLore is an AI-native structured memory server for software projects, enabling AI agents to record, recall, and organize decisions, specs, and knowledge with code context attached — all stored locally in a .linklore/ SQLite database. Key capabilities include:
Project Setup & Config:
initto initialize a project,configfor settings/identity/session, anddoctorfor integrity checks.Memory Operations:
add(create lore/docs with tags, levels, checklists, batch),edit(append, section replace, overwrite, supersede),rm(trash/permanent delete),restore(recover trashed items), andlog(history).Linking & Navigation:
link/unlinkfor relationships (related, flow, supersede);doc_flow(flow-chain view),doc_map(network overview), anddoc_rollup(AI-summary draft from linked lore).Query & Dashboard:
show(search by query/tag/status/time),brief(project dashboard, call at session start), andlog(history).Maintenance:
status(detect code-doc drift via git-diff),cleanup(find duplicates),doctor(data integrity repair).Sharing & Collaboration: Openbox-based sharing:
member(manage openboxes/members),send(share items),import(retrieve from openbox).Backup & Sync:
push/pullfor personal server backup/restore.Local Operations:
localto move/copy/show items between local LinkLore projects.Feedback:
reportto send feedback/bugs.
Provides Google login for authentication to enable personal backup or team sharing of project memory.
LinkLore
MCP server for project memory — lets AI agents record decisions, pitfalls, and specs as they work, and recall them next session with code context attached.
Built for agents, not humans: every tool here is designed for usability by an AI, not readability for a person.
Why LinkLore
Every new session, an AI agent starts from zero — decisions get re-litigated, pitfalls get rediscovered, and "why is it done this way?" gets answered by guessing. Markdown notes don't fix this: they're written for people, they drift from the code, and no agent reads them reliably.
LinkLore is structured memory that the agent itself reads and writes through MCP tools:
Two record types — lore (decisions, pitfalls, journal) and doc (specs, plans), bidirectionally linked to each other and to code files.
Local-first — everything lives in a
.linklore/SQLite store inside your project. No account needed to start.Code context — records link to source files; git-diff-based stale detection flags memory that the code has outrun.
Cheap recall — query by meaning, tag, file, status, or period; a session-start
brief()returns open items and hotspots in a few hundred tokens.
Related MCP server: tentra
Quick Start
Runs straight from PyPI — no install or clone (uvx llre).
Claude Code
claude mcp add llre -- uvx llreAny other MCP client (Cursor, Codex, Windsurf, …) — add to the project's MCP config (e.g. .mcp.json):
{
"mcpServers": {
"llre": {
"command": "uvx",
"args": ["llre"]
}
}
}Then initialize, once, in the project root:
uvx llre initOr let your agent do all of it — this is a server meant to be set up by an AI agent rather than typed in by hand. Paste this as your first message to Claude Code, Cursor, Codex, or any other MCP-capable agent:
Set up LinkLore (an MCP server for project memory) in this repo. It's the PyPI package
llre, run viauvx llre— no install or clone needed. Register it as an MCP server for whichever client you are (Claude Code:claude mcp add llre -- uvx llre; otherwise add{"mcpServers": {"llre": {"command": "uvx", "args": ["llre"]}}}to this project's.mcp.json), then runuvx llre inithere. Ask me first before runninguvx llre login— it opens a browser for Google login and is only needed for personal backup or team sharing.
The Loop
How an agent actually uses it, in order:
start a session → brief() open items, recent activity, hotspots
about to touch code → show(file=..., query=…) decisions & pitfalls tied to that area
decided / got burned → add(type='lore', …) record it, linked to the files involved
spec or plan → add(type='doc', …) instead of a standalone .md
something changed → edit(id=..., …) append, replace a section, or supersede
memory getting messy → doctor() · cleanup() integrity check, duplicate detection
back up / share → push() · openbox(…) your server space · invite-only shared boxesAvailable Tools
23 tools. Call any of them with help=True for full usage.
Category | Tool | What it does |
Setup |
| Set up |
Project |
| Session-start dashboard — open items, recent activity, hotspots |
Project |
| Code↔doc sync drift detection (git-diff based) |
Project |
| Project settings, external sources, session pin |
Diagnostics |
| Data integrity check; |
Record |
| Create lore or doc; |
Record |
| Modify an item — append (default), replace a section, overwrite, or supersede |
Record |
| Delete — recoverable trash by default, |
Record |
| Recover from trash; without |
Record |
| Move/copy/view items across sibling local workspaces |
Search |
| Query by id, text, tag, status, file, or period; graph and tag views |
Search |
| Change history |
Link |
| Connect two items (lore↔lore, doc↔doc, doc↔lore) |
Link |
| Disconnect two items |
Doc view |
| Render a doc's flow chain in order |
Doc view |
| Overview of the full doc link network |
Doc view |
| Collect lore linked to a doc into an AI-summary draft |
Cleanup |
| Detect near-duplicate lore/doc candidates |
My server |
| Back up the local store to your own server space |
My server |
| Restore from your own server space |
Openbox |
| All cross-owner sharing behind one gate — push/pull, browse, invite/join, roles |
Feedback |
| Send feedback or a bug report straight to the team |
The full guide ships inside the product, as lore — LinkLore documents itself with itself. After init(), your project carries it as a read-only source:
show(tag='guide')It updates with the package: upgrade llre and brief() tells you what changed.
Repository Layout
mcp/— the readable portion of the MCP server source (PyPI:llre)plugin/— Claude Code plugin wrapper
Source Model
Mostly readable, core compiled. This repository publishes the MCP server's source in the clear — the entry point, i18n catalogs, base utilities, and the onboarding guide — under the Elastic License 2.0. The core modules (search, ranking, contradiction detection, the tool logic) ship as compiled extensions in the published wheel rather than plaintext source, so "read every line and fork it" isn't 100% true — but the majority is readable, and you can run it, self-host it, and use it commercially inside your own org. We'd rather be upfront that the crown-jewel logic is compiled than pretend it's fully open.
This tree is not buildable on its own (the compiled modules are not here) — install the published package instead: uvx llre.
Research
LinkLore is the case study of the paper "When the Agent Is the User: Agent-Driven Evolution of AI-Native Tools" — defining the AI-Native Tools category, where the AI agent is the primary operator.
DOI: 10.5281/zenodo.21216858 · citation: CITATION.cff
License
Elastic License 2.0. Free to install and use; you may not provide the software as a competing managed service, circumvent license-key functionality, or remove licensing notices.
Team sync, cross-project sharing, and cloud backup are provided by the backend service at linklore.io (not part of this repository).
Available Tools
24 toolsaddB
add(type='lore'|'doc', title, msg=) — lore=journal entry/decision, doc=spec/plan. 2 linking args: links=unified (code files·dc/lr id·title auto-classified, flow=True for a doc chain) · push_to=send to a box (not a link). items=['a','b']→checklist(0/N, doc only) · items=[{...}]→batch creation(both lore·doc — use this for bulk, quieter nudges than N single calls). level 1~4(lore): 1=note 2=verified 3=pinned(shown in brief) 4=global(all projects). details help=True
Inputs: msg, flow, help, tags, +8 more. Returns: result
briefB
[read-only] project dashboard — call at the start of every session.
external source 🔔 = a new push has arrived. receive it with import(openbox=). options: dismiss(turn off a nudge), undismiss(restore it), help.
Inputs: help, dismiss, undismiss. Returns: result
cleanupC
[read-only] cleanup(type='lore'|'doc') threshold(=0.85), status(=open), help - duplicate candidates.
Inputs: help, type, status, threshold. Returns: result
configC
Project settings (openbox source options) + identity + session pin.
Sharing lives in separate tools: send/import (openbox) - member (members/subscriptions).
actions: sources/option/sync/forget/whoami/version/pin/unpin/sessions/projects/delete_project. help=true for details.
Inputs: dir, help, name, email, +8 more. Returns: result
doc_flowC
[read-only] doc_flow(id) — renders a doc's flowLink chain in order (journey view).
Inputs: id, help. Returns: result
doc_mapC
[read-only] doc_map(oneline) — overview of the full doc link network.
Inputs: help, oneline. Returns: result
doc_rollupB
[read-only] doc_rollup(id) — collects lore linked to a doc into an AI-summary draft.
Inputs: id, help. Returns: result
doctorA
doctor() - checks project data integrity (oldId/newId, files[] paths, link targets).
doctor() read-only diagnostics (default) doctor(action='fix') automatically repairs any issues found
Inputs: help, action. Returns: result
editA
edit(id, action=, ...) — 4 write modes (action). If action is omitted, defaults to append (msg alone works too).
Write modes (action, tool option = behavior match):
append (default, non-destructive) → edit(id, action='append', msg='new content') # or omit action, msg alone — appends to the end of body, keeps the rest
section (partial edit) → edit(id, action='section', section='heading', msg='new section content') # replaces just that section (existing heading only — errors if missing; to add a new section use append), keeps the rest
overwrite (full replace) → edit(id, action='overwrite', msg='new body' [, tags=[...], ...]) # old body kept in history(recoverable via log). tags/items(doc only)/links are also fully replaced in the same call
supersede (follow-up publish) → edit(id, action='supersede', msg='new conclusion') # creates a new id, old one head=False
(action= is one of append|section|overwrite|supersede. Unspecified('') means append.)
Other:
mark as dropped → edit(id, status='dropped') (still shown in search)
level (lore importance) → edit(id, level=N): 1=note(default) 2=verified 3=pinned(shown in brief) 4=global(all projects)
supersede chain (keep old + new head) → edit(id, action='supersede', ...) (or add(type='lore'|'doc', relates=old_id, ...))
supersede by an existing item (reconcile a contradiction) → link(a=X, b='lr-Y'|'dc-Y', action='supersede') (X→head=False)
permanent delete → rm(id)
Inputs: id, msg, flow, help, +8 more. Returns: result
importA
import(id, openbox) — bring an openbox's footprints into the home base (the receiver picks what).
import(openbox='ai-team') refreshes the openbox cache (remote/) only, home base untouched
import(id='lr-x'|'dc-x', openbox='ai-team') refreshes the cache + absorbs into the home base (new local id, provenance metadata) (space= is an alias for openbox= — both work.) ⚠️ moving items between local projects/workspaces is local(), not import()
Inputs: id, help, space, openbox. Returns: result
initB
init(blueprint='') — set up .linklore in this directory (starts local footprint memory).
init() basic setup
init(blueprint='X') apply a blueprint
project_dir: set up .linklore in another folder (creating a boundary is init-only). setting up there doesn't change this session's base project — to keep working there, config(action='pin').
Inputs: help, blueprint, project_dir. Returns: result
linkA
link(a, b, action=) — connect two items (dc↔dc / lr↔lr / dc↔lr auto-detected, prefix matching OK). a/b may also be a file path or an existing title — non-id-shaped values are auto-classified (same as links= in add/edit). Undo (inverse) = unlink(a, b) — same arguments.
action has 3 modes (extends the member/config(action=) convention):
action='related' (default, same if omitted) → mutual link (symmetric). Same as link(a, b) — dc↔dc/lr↔lr/dc↔lr
action='flow' → not a mutual link but document order (a→b direction, doc↔doc only): read a, then b.
action='supersede' → "a is replaced by b" — a=old (dropped, head=False/dropped), b=target (alive, must be an existing item — none is created). lore↔lore or doc↔doc only.
Inputs: a, b, help, action. Returns: result
localA
local(action, id, to='workspace-path', from_dir='') — directly operate on another LinkLore project on the same disk (works on both lore and doc).
action='move' move to another workspace (source deleted; createdAt/files/tags/level/status/body/items preserved) action='copy' copy to another workspace (source kept, same id retained — rejected if the target already has it) action='show' view another workspace's content (read-only, from_dir required — query/tag/type/status passed through) ⚠️ For 2+ items, the default is preview only — use confirm=True to execute (a single item runs immediately). cf. sharing across a server/team is the openbox (send/import) — local() never talks to a backend, it only works within your own disk. ⚠️ only between projects you own — if the target is genuinely someone else's/another team's, don't use local() just because you have filesystem access; go through send() (the recipient must import() — a review gate).
Inputs: id, to, tag, help, +6 more. Returns: result
logD
[read-only] log() id, period('Nh' hours|'Nd' days|'YYYY-MM-DD' UTC), sort=oldest, max, plumbing: help
Inputs: id, max, help, sort, +1 more. Returns: result
memberA
member(action) — openbox (shared space) members and permissions. An openbox is a project's shared space (invite-only, not 'public').
member('new', openbox=, display_name=) create an openbox (you = owner)
member('invite', openbox=, role=, kind=) issue an invite code (owner). kind='project' = project member
who=<member_id> — reissue invite (targets an existing project identity, rotates the session)
member('join', code=, display_name=) join with an invite code (first join)
member('subscribe', openbox=, url=) connect an openbox you're already a member of via URL (authenticates as your identity)
member('list', openbox=) list members
member('role', openbox=, who=, role=) change a member's role (viewer↔member, owner)
member('transfer', openbox=, who=) transfer owner (who=member_id, double confirmation)
member('leave', openbox=) leave yourself (owner gets 409, transfer first) remove: rm(member='<member_id>', openbox='') delete the whole openbox: rm(openbox='') — owner only, double confirmation (openbox= is an alias for name= — both work.)
Inputs: url, who, code, help, +7 more. Returns: result
pullA
pull(id) — restore the home base from my server (works across devices too). Like git pull: server → local. To bring things from an openbox use import — pull is for my server only. If local is newer/same it's skipped; a real conflict (both sides changed) is shown separately (local kept).
pull() my whole server → home base
pull(id='lr-x'|'dc-x') restore a selection (moving items between local workspaces is local(), not pull())
Inputs: id, help. Returns: result
pushA
push(id) — upload the home base to my server (visible to me only). Like git push: local → server. Not for sharing — to share with others use send (push has no target argument by design, so it cannot share).
push() the whole home base → server
push(id='lr-x'|'dc-x') a single item
push(id=['lr-x','dc-y']) a batch Take down (inverse): rm(sent='lr-x'|'dc-x') — removes the server copy only, home base untouched. (moving items between local workspaces is local(), not push())
Inputs: id, help. Returns: result
reportB
report(msg) — send feedback/a bug straight to the team. Works whether or not you're logged in (if logged in, your identity is attached automatically — never forced).
Inputs: msg, help. Returns: result
restoreB
restore(id) — recover trashed lore/doc. no id lists the trash.
Inputs: id, help. Returns: result
rmA
rm — unified delete. Target is chosen by argument (action= is an explicit alternative, auto-detected by which argument is present when omitted).
Deleting items (lore/doc) — id is a single value or a list (batch)
trash (recoverable, default) →
rm(id)→ restore(id) to recoverpermanent delete (unrecoverable) →
rm(id, force=True)batch delete →
rm(id=['lr-x','dc-y'], force=True)mark as dropped (stays in search) →
edit(id, status='dropped')supersede (keep old + new head) →
add(type='lore'|'doc', relates=old_id, ...)
Taking down something you sent (reverse of push/send, doesn't touch the source of truth)
rm(sent='lr-x'|'dc-x', openbox='ai-team')take down from an openbox (sender only)rm(sent='lr-x'|'dc-x')take down from my server(or action='sent', sent= is still required — action= is just the explicit form)
Removing a member (openbox owner only)
rm(member='<member_id>', openbox='ai-team')(or action='member')
Deleting an entire openbox (shared space) (owner, unrecoverable → always asks to confirm)
rm(openbox='ai-team')1st call: returns a warning+question (nothing executes)rm(openbox='ai-team')identical 2nd call → that itself is the confirmation, actually deletes (no confirm= param)(space= is an alias for openbox=)
(or action='openbox', openbox= is still required)
⚠️ action='openbox' cannot combine with id= (explicit rejection) — deleting an item and deleting an entire shared space are separate operations. id alone (action unspecified) goes to regular item delete.
Inputs: id, help, sent, force, +4 more. Returns: result
sendA
send(id, openbox) — share a footprint to an openbox (visible to others). Both id and openbox are required.
send(id='lr-x'|'dc-x', openbox='ai-team') share one item
send(id=['lr-x','dc-y'], openbox='ai-team') a batch Not my-server backup — that's
push. The home base is untouched — this copies into the openbox. Take down (inverse): rm(sent='lr-x'|'dc-x', openbox='ai-team') — sender only, home base untouched. (space= is an alias for openbox= — both work)
Inputs: id, help, space, openbox. Returns: result
showB
[read-only] show() — target/scope: query·type·openbox / filters: tag·level·status·file·period('Nh' hours|'Nd' days|'YYYY-MM-DD' UTC)·source_id / result shape: sort·max·oneline·superseded / mode: action / plumbing: help
Inputs: max, tag, file, help, +11 more. Returns: result
statusA
status() — detects code↔doc sync drift (git-diff based). Not for reading lore/doc content → use show()/brief().
since, action(''|'ack'|'reset'), ack, reset, help. (2026-07-09: the dead params auto=/sync= and ack_all= were removed — ack='all' does the same job, matching the reset='all' convention)
Inputs: ack, help, reset, since, +1 more. Returns: result
unlinkB
unlink(a, b, action=) — disconnect two items. Symmetric with link() (action='' default|'flow', also accepts file paths the same way).
Inputs: a, b, help, action. Returns: result
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 24 tools
Most tools have distinct purposes, but a few overlap conceptually, such as 'doc_flow' vs 'doc_map' for document link viewing, and 'show' vs 'brief' vs 'status' for display. However, detailed descriptions help agents differentiate.
Tool names consistently use snake_case but mix verbs (e.g., push, pull) and nouns (e.g., config, doc_flow). The pattern is not strictly verb_noun, but the naming is readable and predictable given the domain.
With 24 tools, the server covers a broad scope (CRUD, linking, sharing, diagnostics). While this is on the high side, it is justified by the feature set, but could potentially be streamlined.
The tool surface covers most lifecycle operations for lore/documents, linking, sharing, and project management. Minor gaps exist, such as no explicit rename tool, but the core workflows are supported.
Maintenance
Related MCP Connectors
Long-term memory for AI coding agents: durable project facts, recalled by every MCP client.
Team memory for AI coding agents and code reviews, reviewed by humans and served over MCP.
Shared AI memory. ChatGPT, Claude, Cursor and any MCP app read and write the same memory.
Hosted MCP memory for coding agents: persistent across sessions, editable markdown, team sharing.
Related MCP Servers
- AlicenseAqualityBmaintenanceMCP-native, local-first memory for coding agents that turns real sessions into reusable decisions, gotchas, and domain knowledge.17136 PyPI7MIT
- AlicenseBqualityDmaintenanceMemory for AI coding agents. Persistent code graph + AI architecture diagrams. 32 MCP tools3299 npm6MIT
- AlicenseAqualityBmaintenanceLocal, searchable project memory for AI coding agents. Markdown source of truth, MCP interface, safe structured updates315Apache 2.0
- FlicenseNot gradedqualityCmaintenancePersistent, self-curating memory for coding agents. It enables local, zero-cost context recall through MCP tools with hybrid retrieval and autonomous consolidation.-