Skip to main content
Glama

LinkLore

PyPI Python License: Elastic 2.0 MCP

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.

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 llre

Any 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 init

Or 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 via uvx 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 run uvx llre init here. Ask me first before running uvx 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 boxes

Available Tools

23 tools. Call any of them with help=True for full usage.

Category

Tool

What it does

Setup

init

Set up .linklore/ in the current directory

Project

brief

Session-start dashboard — open items, recent activity, hotspots

Project

status

Code↔doc sync drift detection (git-diff based)

Project

config

Project settings, external sources, session pin

Diagnostics

doctor

Data integrity check; action='fix' auto-repairs

Record

add

Create lore or doc; items=[{...}] batch-creates

Record

edit

Modify an item — append (default), replace a section, overwrite, or supersede

Record

rm

Delete — recoverable trash by default, force=True for permanent

Record

restore

Recover from trash; without id lists the trash

Record

local

Move/copy/view items across sibling local workspaces

Search

show

Query by id, text, tag, status, file, or period; graph and tag views

Search

log

Change history

Link

link

Connect two items (lore↔lore, doc↔doc, doc↔lore)

Link

unlink

Disconnect two items

Doc view

doc_flow

Render a doc's flow chain in order

Doc view

doc_map

Overview of the full doc link network

Doc view

doc_rollup

Collect lore linked to a doc into an AI-summary draft

Cleanup

cleanup

Detect near-duplicate lore/doc candidates

My server

push

Back up the local store to your own server space

My server

pull

Restore from your own server space

Openbox

openbox

All cross-owner sharing behind one gate — push/pull, browse, invite/join, roles

Feedback

report

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 tools
addB

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

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 recover

  • permanent 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

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    B3.1/5.0

    Scored across 24 tools

    Disambiguation4/5

    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.

    Naming Consistency3/5

    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.

    Tool Count3/5

    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.

    Completeness4/5

    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

    ActivityNo data
    ResponsivenessOutdated data

    Related MCP Connectors

    Related MCP Servers