my-memory
Integrates with Forgejo as remote Git hosting, providing OAuth 2.1 authentication for public clients, webhook handling for pushes, and support for pushes from Forgejo's web editor via rebase.
Uses Git as the source of truth for notes, committing every write with client attribution and trailers, maintaining history, and handling concurrent writes via optimistic concurrency and rebase.
Stores binary attachments in Git LFS, with companion notes containing extracted text (e.g., PDFs via pdftotext) so they remain searchable.
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
my-memory
A self-hosted memory for AI agents. Your notes are a folder of Markdown files in git; my-memory serves them over MCP so any agent (Claude Code, Codex, claude.ai, your own bots) can search, read and write them. Every write is a git commit attributed to the client that made it.
Keep this file and README.es.md in sync: when you edit one, update the other.
Git is the source of truth. Search runs on a SQLite index (catalog, FTS5 full text by section, wikilinks) that can be rebuilt from git at any time.
One commit per write, authored by the client:
Claude Code (laptop),Alex (Claude)…. The commit message is the reason the agent gave, plusClient:/Tool:/Session:trailers.Safe for several agents at once. One writer queue, optimistic concurrency (
hashfromread), and pushes from elsewhere (your PC, Forgejo's web editor) integrated by rebase.Your conventions, not ours. The notes repo declares its folders, required frontmatter and filename rules in
memory.toml; itsAGENTS.mdis sent to every client as MCP instructions, with a short index.Two front doors. A private listener for static per-client tokens, file uploads and the git webhook. A public listener that only accepts OAuth 2.1 tokens (dynamic client registration and PKCE, sign-in delegated to Forgejo) for remote clients like claude.ai.
Attachments. Binaries go to git LFS with a companion note holding their text (PDFs via
pdftotext; scanned PDFs and images via local OCR withtesseract, when installed), so they are searchable too.Typed links.
works_at:: [[acme]]says what a link means, andlinkscan filter by it.Optional search by meaning. Point
[embeddings]at any OpenAI-compatible embeddings endpoint (Ollama or llama.cpp on the same CPU, TEI, OpenAI…) andsearchfuses full text with vector similarity, so "car" finds the note that says "vehicle". No model runs inside the server and no GPU is needed; without it, search is full text only.
How it works inside (index, write flow, concurrency, auth): docs/design.md.
Give your agent one link
Once the server is connected to an agent (see Connecting clients), send it this, and nothing else:
https://raw.githubusercontent.com/PabloPC05/my-memory/main/docs/agent-setup.mdThe agent reads the setup guide and installs your memory on its own host: it finds the memory among its tools, proposes importing what it already knows about you, installs standing instructions and, where the host allows it (Claude Code, your own agents), blocks it from saving anywhere else. It asks for one approval before changing anything. Inspired by Notion's agent setup page.
On Claude Code it can also install, if you want, a hook that brings relevant notes to the agent without it having to think of searching (as Supermemory does): before each message, inject-memory.mjs searches your memory for it and adds the best few matches to the turn, framed as information to check, not instructions, within a small budget. It needs MY_MEMORY_URL and a read-only MY_MEMORY_TOKEN, and adds nothing on any error or timeout.
Related MCP server: Cortex
Tools
Tool | What it does |
| Short index, or every note in a folder with description, aliases and last change |
| Full-text search by section (accents ignored, prefixes match) with snippets; hybrid with search by meaning when |
| A note or one section, with its |
| Outgoing |
| Commits that touched a note: who, when, why |
| Latest changes to the whole memory: per commit when, who (author and client), tool, reason and the notes it touched; filters |
| Maintenance worklist, worst first: bad frontmatter and superseded notes outside the archive, broken links, look-alike duplicates, old inferences, missing pages, unlinked mentions, orphans, stale and oversized notes. Counts per kind; filter by |
| Who is connected: every token and OAuth app with last seen (time and tool), writes in the last 7 and 30 days, and the |
| New note; unique names across folders; refuses look-alike duplicates unless |
| Exact text replacements, all or nothing |
| Rewrite a whole note; needs the |
| Rename or move; rewrites every |
| Remove a note (it stays in history); refused while other notes link to it |
| Undo a commit from |
| Store a file in LFS with a searchable companion note |
Clients with a read-only token only see the read tools: list, search, read, links, history, recent, lint and clients.
lint is the cheap half of keeping the memory tidy (Karpathy's "LLM wiki": ingest, answer, lint). It is computed from the index and git dates in milliseconds, without a model, so the agent gets candidates instead of reading every note; judging them (is this a contradiction, does that inference still hold, merge or link?) is the agent's half. See docs/design.md.
The server also offers the agent setup guide to every connected client, offline (it ships with the package): as the MCP prompt setup ("Set up this memory on your host") and as the resource memory://agent-setup, both with the Markdown of docs/agent-setup.md. The server instructions mention them, so an agent that finds itself on an unconfigured host can follow them without being given the URL.
Notes format
---
name: Universidad
description: Carrera, asignaturas de este curso y cómo las lleva
aliases: [uni, carrera]
---
# Universidad
Text, with links like [[otra-nota]], [[otra-nota#Sección|texto]] or ![[documento.pdf]].File names (without .md) must be unique across the repo, so [[name]] always resolves; this is what Obsidian does with "shortest path" links.
A link can also say what it means, with Dataview-style inline fields: works_at:: [[acme]] on its own line, [lives_with:: [[sam]]] inside a sentence, or works_at: "[[acme]]" in the frontmatter. links returns the relation and can filter by it, so "who works at Acme" is one call.
memory.toml at the root of the notes repo (every key is optional):
[folders]
allowed = ["me", "people", "topics", "projects", "archive"]
archive = "archive" # excluded from list/search unless asked
index_full = ["me"] # listed note by note in the short index
root_files = ["AGENTS.md", "README.md"]
[notes]
required_fields = ["name", "description"]
filename_pattern = "^[a-z0-9]+(-[a-z0-9]+)*\\.md$"
superseded_field = "superseded_by"
max_description = 200
[instructions]
file = "AGENTS.md"
[attachments]
page_heading = "Page {n}"
ocr_notice = "Text recognised with OCR ({languages}); it may contain errors."
[relations]
allowed = ["works_at", "lives_with"] # warn on other keys; without it, keys in use are suggested
[lint]
exclude = [] # folders left out of every check (root files and the archive always are)
orphan_exclude = ["me"] # never reported as orphans; defaults to folders.index_full
stale_days = 180 # unchanged for longer → stale
inference_days = 90 # "(deducido, 2026-01-31)" older than this → old_inference
inferred_markers = ["deducido"] # provenance words that mark an inference
max_bytes = 16384 # bigger → oversized (attachment notes are exempt)
missing_page_min = 2 # a missing target linked from this many notes → missing_page
mention_min_length = 4 # shortest name or alias looked for as an unlinked mentionRunning it
Requirements: Node.js 24, git, git-lfs, and optionally poppler-utils (pdftotext for PDF text, pdftoppm for OCR of scans) and tesseract with the language data you want (for OCR of scanned PDFs and images). Missing tools are reported at startup (ocr.tools in the log) and only make that extraction unavailable.
npm ci && npm run build
my-memory token add laptop --name "Claude Code (laptop)" --scope write --file tokens.toml
my-memory serve --config config.toml
my-memory clients --config config.toml # who is connected: last seen, writes, setup version (--json for scripts)
my-memory lint --config config.toml # maintenance worklist (--json, --kind stale,orphan, --folder, --limit)Last-seen times are kept in activity.sqlite in the data dir (written at most once a minute per client; it is not in the notes repo) and the write counts come from the Client: trailers of the git history.
config.toml:
data_dir = "/var/lib/my-memory"
public_url = "https://memory.example.com" # only with the public listener
tokens_file = "tokens.toml"
webhook_secret_file = "webhook-secret" # paths, or credential:NAME for systemd credentials
[remote] # omit to keep the notes repo local
url = "http://127.0.0.1:3000/me/notes.git"
username = "my-memory"
token_file = "forgejo-token"
[listen.private]
address = "127.0.0.1"
port = 8782
[listen.public] # behind your TLS reverse proxy
address = "127.0.0.1"
port = 8781
[oauth] # required by the public listener
forgejo_url = "https://git.example.com" # as your browser reaches it
forgejo_internal_url = "http://127.0.0.1:3000" # as the server reaches it
client_id_file = "oauth-client-id" # an OAuth2 app registered in Forgejo,
client_secret_file = "oauth-client-secret" # redirect URI <public_url>/oauth/callback
allowed_users = ["me"]
[ocr] # every key is optional
enabled = true
languages = "spa+eng" # tesseract language codes; default "eng"
max_pages = 50 # pages of a scanned PDF that are OCR'd
page_timeout_seconds = 120 # per page (render + recognition) or image
# dpi = 300; tesseract = "tesseract"; pdftoppm = "pdftoppm"
[embeddings] # optional: search by meaning; omit for full text only
url = "http://127.0.0.1:11434/v1/embeddings" # any OpenAI-compatible endpoint (Ollama, llama.cpp, TEI, OpenAI…)
model = "nomic-embed-text"
# api_key_file = "embeddings-key" # sent as a Bearer token, if the endpoint needs one
# dimensions = 512 # for models that can shorten their output
# batch_size = 32
# timeout_seconds = 60
query_prefix = "search_query: " # some models want their inputs marked (nomic, e5)
document_prefix = "search_document: "Uploads for attach: curl -T file.pdf -H "Authorization: Bearer $TOKEN" http://host:8782/uploads/file.pdf returns an upload_id. There is no size limit.
NixOS
{
inputs.my-memory.url = "github:<owner>/my-memory";
# in your configuration:
imports = [ inputs.my-memory.nixosModules.default ];
services.my-memory = {
enable = true;
remote = { url = "http://127.0.0.1:3000/me/notes.git"; tokenFile = "/run/secrets/forgejo-token"; };
listen.private = { address = "127.0.0.1"; port = 8782; };
tokensFile = "/var/lib/secrets/my-memory/tokens.toml"; # root:my-memory 0640
# ocr = { enable = true; languages = [ "spa" "eng" ]; maxPages = 50; pageTimeoutSeconds = 120; dpi = 300; };
};
}After my-memory token add … --file <tokensFile>, run systemctl reload my-memory. The package brings pdftotext, pdftoppm and tesseract; the module installs the language data listed in ocr.languages (default Spanish and English), and builds that tesseract only when ocr.enable is on.
For search by meaning, add embeddings = { url = "http://127.0.0.1:11434/v1/embeddings"; model = "nomic-embed-text"; queryPrefix = "search_query: "; documentPrefix = "search_document: "; }; (with apiKeyFile if the endpoint needs a key), for example next to services.ollama.
Search by meaning
With [embeddings], every section of every note gets a vector from the endpoint, stored in index.sqlite. search then takes mode: hybrid (default) fuses the full-text ranking and the vector ranking with Reciprocal Rank Fusion, text is the plain full-text search, semantic uses vectors only. The answer has the same shape as always.
Writes never wait for it. Sections are embedded in the background after each commit or sync; until then they are found by text. Only sections whose text changed are embedded again, and changing
model,dimensionsordocument_prefixembeds everything again.If the endpoint is down, writes work as usual, search falls back to full text with a warning in the log, and pending sections are retried with backoff.
Status in the log:
embeddings.statusat startup (model, vectors, sections pending),embeddings.doneafter each run,embeddings.failedwhile the endpoint fails.my-memory reindexalso embeds the sections that are missing;--reembedembeds them all again. With acredential:key it cannot read outside systemd, it skips embedding and the service does it when it starts.On a small CPU-only server, Ollama with
nomic-embed-text(multilingual:bge-m3ormultilingual-e5-small, the e5 ones with prefixesquery:andpassage:) embeds a few thousand sections in minutes, once.
Connecting clients
# Claude Code
claude mcp add --transport http memory http://host:8782/mcp --header "Authorization: Bearer $MY_MEMORY_TOKEN"# Codex (~/.codex/config.toml)
[mcp_servers.memory]
url = "http://host:8782/mcp"
bearer_token_env_var = "MY_MEMORY_TOKEN"claude.ai: Settings → Connectors → Add custom connector → https://memory.example.com/mcp.
Then give the agent the setup link (Give your agent one link).
One credential per agent
my-memory attributes every write (the git commit author and its Client: trailer), every log line and the clients view to the credential the request used. So each agent must connect with its own credential. If two agents share one, nobody can tell who wrote what, clients cannot tell them apart, and a leaked key cannot be revoked without cutting off the other agent too.
Static tokens: create one per agent and never share it:
my-memory token add <id> --name "<Agent name>" --scope write|read --file tokens.toml. OAuth clients (for example claude.ai connecting directly) get their own identity automatically.Behind a gateway or proxy that joins several MCP servers into one endpoint, the gateway is the client my-memory sees. Give the gateway one key per agent and map each key to its own my-memory token (same public URL, a different key per agent); otherwise every write shows the gateway's name. This pattern has been done with Caddy plus agentgateway, but any gateway that can pick the upstream credential from the incoming key works.
Check it: run
my-memory clientsor call theclientstool. Each agent must appear on its own row, with its last activity and its writes.
Moving an existing repo to wikilinks
my-memory migrate <clone> shows what would change: relative Markdown links become [[wikilinks]], name collisions are renamed, and frontmatter that is not valid YAML gets quoted. Add --apply to commit it. Only those spans change.
Development
npm ci
npm test # vitest: unit, integration (real git repos) and HTTP/OAuth end to end
npm run typecheck
nix flake check # builds the package and runs the NixOS VM testLicense
Apache-2.0.
This server cannot be deployed
Maintenance
Related MCP Connectors
Serve a folder of Markdown notes as an MCP server: hybrid search, reading, and sourced answers.
Markdown-based note-taking with a hosted MCP server. Your notes serve you and your AI.
Google Keep-style notes app with an MCP server for AI agents to read/write notes.
OAuth-protected, read-only-by-default MCP server for provenance-labeled QuillCaddie project memory.
Related MCP Servers
- FlicenseCqualityDmaintenanceGit-backed MCP server for creating and maintaining an Obsidian-style markdown knowledge base with full CRUD, search, and git sync.7-
- AlicenseNot gradedqualityBmaintenanceA dynamic, governed memory layer for Markdown notes that serves knowledge to AI clients and humans through a secure MCP server, with scoped access, git-audited changes, and optional LLM-powered semantic search.Apache 2.0
- AlicenseAqualityCmaintenanceMCP server for managing a local, domain-agnostic knowledge base using Markdown notes with frontmatter. Enables AI agents to capture, read, search, link, and maintain notes with atomic writes and privacy controls.13MIT
- AlicenseNot gradedqualityDmaintenanceAn MCP server that gives AI agents read and write access to a folder of Markdown notes, supporting full-text search, reading, listing, and writing notes. It is deliberately small, with no dependencies beyond the MCP SDK, and includes proper path traversal protection.MIT