Synapto
Provides persistent memory for LangGraph agents, enabling cross-session recall and structured memory management.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Synaptorecall how Hermes handles messaging"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Synapto
Your AI agent forgets everything between sessions. Synapto fixes that.
Flat-file memory (MEMORY.md) doesn't scale — no search, no structure, no decay. Synapto gives any MCP-compatible agent a real memory: store once, recall by meaning, watch bad memories fade and good ones persist.
# remember — while working inside ~/src/acme/api
"acme/api publishes events through an outbox table, never directly from the request handler"
# recall — weeks later, different session, same repository
"How does acme/api publish events?"
→ [stable] acme/api publishes events through an outbox table ... (score=0.94, trust=0.65)Works with Claude Code, Cursor, Windsurf, Codex, LangGraph, Agno, or any MCP client.
Each repository gets its own memory partition automatically: the tenant is derived from the
working directory's git remote, so acme/api never sees acme/web's notes unless you ask.
Cross-agent handoffs
Pass work between Codex, Claude Code, Cursor, and other agents in plain language. Synapto stores the structured state under the hood, so the next agent can continue from a memory ID instead of a long pasted brief.
You → Codex: Plan this feature and leave a handoff for Claude to implement.
Codex → You: Handoff created for Claude: b0e1506e-d1b7-4bee-9223-4d0f8d18a1b2
You → Claude: Continue from Synapto handoff b0e1506e-d1b7-4bee-9223-4d0f8d18a1b2.
Claude → You: I read the handoff, fetched its context, and can continue.What you say | What Synapto does |
"Codex, leave this for Claude." | Looks the task's packet up by |
"Claude, continue from this handoff ID." | Fetches the full memory with |
"Any handoffs for me?" | Looks packets up by |
"Mark it ready for review." | Extends the same packet with |
See Cross-agent handoffs for the lifecycle, schema, and Claude/Cursor recipes.
Related MCP server: mcp-memory
Try it in 60 seconds
Docker:
git clone https://github.com/ramonlimaramos/synapto.git && cd synapto
docker compose up -d
docker compose exec synapto synapto search "hello world"Local:
pip install synapto
createdb synapto && psql -d synapto -c "CREATE EXTENSION vector;"
synapto init
synapto search "hello world"What it does
Search — Ask a question, get the best memory. Behind the scenes, three signals (vector similarity, full-text, and compositional algebra) are fused into one score. You just call recall.
Scopes — A memory can declare where it applies: repo:acme/api, language:python, skill:code-review, global:all. recall(scopes=[...]) returns only what applies to the context you are in, and a metadata_filter narrows further ("every finding with failure_class = missing_docstring", or every memory whose products list contains inbox, with a true total, not a page size). Scopes gate, metadata describes: a scope is a condition the reader must name, a metadata facet is a fact about the memory that never hides it from an unrelated query.
Provenance — Every memory records who wrote it: human, agent, or consolidation. Recall can filter by origin, and forget refuses to delete a human-authored memory unless told explicitly.
Graph — Entities are auto-extracted and linked. Ask "what depends on Kafka?" and get an answer via graph traversal, not keyword guessing.
Decay — Core memories live forever. Ephemeral notes fade in hours. Working context lasts about a week. Memories that get used stay alive; unused ones sink.
Trust — Mark memories as helpful or not. Bad info gets demoted 2x faster than good info gets promoted. Over time, your memory self-cleans.
Handoffs — Tell one agent to leave work for another in natural language.
Synapto turns that into a structured handoff memory, and the receiver continues
with get_memory, context_ids, and follow-up updates.
Quickstart
Prerequisites
Python 3.11+
PostgreSQL 14+ with pgvector
Redis 7+
Install and initialize
pip install synapto
createdb synapto && psql -d synapto -c "CREATE EXTENSION vector;"
synapto init # or: synapto init --interactiveConnect to your agent
The recommended way is uvx with --refresh — every restart pulls the latest version from PyPI, no manual upgrades:
Claude Code — register the server at user scope, so it is available in every project:
claude mcp add --scope user synapto \
-e CLAUDE_CODE_DISABLE_AUTO_MEMORY=1 \
-- uvx --refresh synapto serveThat writes the following entry to ~/.claude.json (the file Claude Code actually reads for user-scoped servers — ~/.claude/.mcp.json is not consulted):
{
"mcpServers": {
"synapto": {
"type": "stdio",
"command": "uvx",
"args": ["--refresh", "synapto", "serve"],
"env": {
"CLAUDE_CODE_DISABLE_AUTO_MEMORY": "1"
}
}
}
}Set CLAUDE_CODE_DISABLE_AUTO_MEMORY=1 for Claude Code so Synapto remains the single memory sink instead of duplicating new memories into Claude's flat-file auto-memory.
Claude Code starts the server in the session's working directory, which is what makes per-repository tenants work: open a session inside ~/src/acme/api and memories land in tenant acme/api. If you run the server from a checkout instead of PyPI, use uv run --project /path/to/synapto synapto serve rather than uv --directory ... — --directory changes the working directory before the server starts, and every session would then derive the checkout's own tenant.
Restart Claude Code after changing the configuration so the MCP subprocess receives the new environment.
Cursor (.cursor/mcp.json):
{
"mcpServers": {
"synapto": {
"command": "uvx",
"args": ["--refresh", "synapto", "serve"]
}
}
}Why
--refresh? Without it,uvxreuses the cached environment across restarts, so a new Synapto release on PyPI will not be picked up until the cache expires or you runuv cache clean synaptomanually.--refreshtellsuvto re-resolve the package on every launch, adding 1–3 seconds to startup in exchange for "always on the latest version" — the right default for an alpha project that ships often. Drop the flag (or pin a version like"synapto==0.2.0") if you want to freeze the version.
Restart your agent. Synapto tools appear automatically, and any future release will be live on the next restart.
Default Memory Routing
Synapto is designed to be the primary memory sink for MCP-compatible agents. Agents should call recall before non-trivial work and call remember when users provide durable context instead of writing new memory into flat files.
User signal | Tool action | Recommended type/layer |
"always X", "never Y", "from now on" | Store as a rule |
|
"don't do X", "that's wrong" | Store as a correction |
|
"we use X for Y", "our architecture is..." | Store as project context |
|
"this sprint", "current PR", "release plan" | Store as active work |
|
"tracked in Linear", "dashboard is..." | Store as external reference |
|
"I work on...", "my preference is..." | Store as user context |
|
subtype is optional and free-form. Recommended values include code_style, workflow, tooling, testing, security, communication, external_system, documentation, role, preference, skill, and constraint.
Tenants and scopes
A tenant is the partition a memory lives in; a scope is where inside that partition it applies. Tenants keep repositories apart, scopes keep a Python rule from firing in a TypeScript file.
Tenants are derived, not typed
Every tool resolves the tenant in this order:
An explicit
tenant=argument — validated, never repaired.Acme/API,git@github.com:acme/api.git, andacme/api/are all rejected with the canonical form named in the error.The
originremote of the git repository containing the server's working directory, normalised toowner/name(https://github.com/acme/api.git→acme/api).[defaults] tenantin the config file, orSYNAPTO_DEFAULT_TENANT.default.
Leave step 3 unset unless you have a reason: default is then an honest bucket for "no repository here", and nothing written outside a checkout is silently attributed to one.
Older installs that stored memories under hand-typed spellings (api, acme-api, acme/api) can be collapsed onto the canonical tenant:
synapto maintain --merge-tenants --dry-run # report the proposed mapping
synapto maintain --merge-tenants --apply # move the memories, record the aliasesEvery target is a canonical, lowercase tenant. A fold from a canonical spelling (api → acme/api) is recorded in tenant_aliases (one hop, never a chain), and every tool follows that alias on reads and writes, so the old spelling keeps working. A legacy spelling the grammar rejects (Acme/API) is folded onto its lowercase form and records no alias — step 1 already refuses it and names the lowercase form — and a store holding only Acme/API is offered acme/api rather than left unreachable. --apply runs the printed plan as one transaction: a group refused midway undoes the ones before it.
Scopes are typed
A scope is a condition: a memory is returned only when the query names a key for every scope type the memory carries (OR within a type, AND across the types the memory itself carries). That is what makes a Python-only rule stay out of an Elixir session even when the query did not mention a language, and it is also why a scope is the wrong place for what a memory is merely about. Facts that describe a memory — the products it concerns, the repositories it cites, its language — belong in metadata (products, repos, language), where a metadata_filter selects them without hiding the memory from every other read. Scopes gate, metadata describes.
A scope is "<type>:<key>". Seven types exist: global, product, repo, language, skill, workflow, area. global:all is the only global key, and it cannot be combined with other scopes on the same memory. area names the discipline a memory belongs to (area:software-engineering, area:finance) rather than a place it applies; a memory with no area applies in every area, a query for one area never sees another area's memories, and a memory that carries an area is returned only by scoped queries that name that area (the rule below) — tag areas deliberately.
remember("Run ruff before every commit", scopes=["repo:acme/api", "language:python"])
recall("pre-commit checks", scopes=["repo:acme/api", "language:python"])A scoped recall returns a memory when it is global:all, or when every scope type the memory declares is satisfied by one of the query's scopes of that type — a memory tagged repo:acme/api and language:python needs both to match; one tagged only language:python needs just the language. Memories stored without scopes are not returned by a scoped recall: unscoped means "never governed", not "applies everywhere" — use global:all for that. A recall without scopes ignores the axis entirely. Unknown types and malformed keys are rejected, never guessed.
domain= still works on remember and recall but is deprecated: it is one free-form label, whereas scopes are typed and plural. New callers should use scopes.
MCP Tools
Tool | What it does |
| Store a memory with optional |
| Search memories by meaning; narrow by |
| Check MCP transport health without touching PostgreSQL, Redis, or embeddings |
| Fetch the complete content and metadata for one recalled memory |
| Fetch complete content for multiple recalled memories |
| Replace, append, or patch fields (including |
| Link two entities ("acme/api" --[publishes]--> "orders.created") |
| Soft-delete a memory; human-authored memories require |
| Mark a memory as helpful or unhelpful |
| Find memory pairs that disagree |
| Walk the knowledge graph (N-hop) |
| Browse known entities |
| View counts and distribution |
| Run decay and ephemeral cleanup |
| Instructions for a task's single handoff packet: look it up by |
| Build the |
Tool Field Limits
Synapto validates known hard limits before hitting the database, so MCP clients get actionable errors instead of raw Postgres exceptions.
Field | Limit |
| Text; no Synapto length limit |
| Max 255 characters |
| Max 20 characters |
| Optional free-form subcategory, max 50 characters |
| Deprecated in favour of |
| Max 20 characters |
| Canonical |
| Up to 20 unique |
| One of |
| A flat JSON object, up to 20 keys. A scalar value means equality; a list of scalars (up to 20) means the stored list contains every element — a list never matches a stored scalar, and a scalar never matches a stored list. Nested objects are rejected because containment on an object would not mean equality |
| Max 20 IDs per call |
| Clamped to 0-1000 characters |
CLI
synapto serve # start MCP server
synapto init # create tables, indexes and extensions
synapto doctor # check postgres, redis, embeddings health
synapto search "kafka topics" # search from terminal
synapto stats # memory statistics
synapto migrate status # show applied/pending migrations
synapto migrate up # apply pending migrations (serve does this on start)
synapto migrate down --to 7 # roll back everything after version 7
synapto maintain --merge-tenants --dry-run # propose a tenant merge; --apply performs it
synapto export -o backup.json # export memories
synapto import MEMORY.md --format markdown # migrate from flat files
synapto migrate-memories # detect and import other agents' memory files
synapto configure-mcp --client claude-code --tenant acme/api # write the MCP entry to ~/.claude.json
synapto configure-mcp --client cursor # write the MCP entry for Cursorconfigure-mcp --client claude-code updates only mcpServers.synapto in ~/.claude.json, keeps every other key in that file, prints the entry that will load, and names ~/.claude/.mcp.json if an older release left one there — Claude Code never read it.
Migrations ship inside the package and synapto serve applies any that are pending when it starts, so upgrading the package is enough to upgrade the schema.
Depth Layers
Layer | Half-life | Example |
| Forever | "Our API uses REST, never GraphQL" |
| ~6 months | "Auth service is in Go, everything else is Python" |
| ~1 week | "Currently refactoring the payment module" |
| ~6 hours | "Debugging: the timeout was 30s, changed to 60s" |
How it works under the hood
When you call recall("kafka patterns"), Synapto runs three searches in parallel and fuses the results:
Vector similarity (pgvector HNSW) — finds semantically close memories
Full-text search (tsvector + BM25) — finds keyword matches
HRR compositional algebra — detects if "kafka" plays a structural role in the memory, not just appears as a word
The scores are combined via Reciprocal Rank Fusion — the HRR similarity enters on the same scale, worth at most one leg for an identical vector and nothing within the noise floor — then multiplied by decay, trust, and a depth-layer weight (core 1.3, stable 1.25, working 1.0, ephemeral 0.7 — chosen by the sweep in docs/eval/layer_sweep.md). Ties are broken by created_at DESC, id, so equal scores come back in the same order every call and the newest memory wins. Filters — tenant, scopes, metadata, origin, layer, subtype — are applied inside the SQL before ranking, so a filtered recall is a smaller search, not a trimmed page.
HRR (Holographic Reduced Representations) also enables queries that no vector database can do:
probe("kafka")— find memories where Kafka is structurally involved (not just mentioned)reason(["kafka", "acme/api"])— find memories about both entities simultaneously (vector-space AND)contradict()— find memory pairs that share entities but say different things
More in docs/hrr.md.
Configuration
Config file: ~/.synapto/config.toml
[postgresql]
dsn = "postgresql://localhost/synapto"
[redis]
url = "redis://localhost:6379/0"
[embeddings]
provider = "" # auto-select (sentence-transformers locally, openai if API key set)
model = ""
device = "" # optional sentence-transformers device override, e.g. "cpu"
[defaults]
# tenant = "acme/api" # fallback used only outside a git checkout; see "Tenants and scopes"
[decay]
ephemeral_max_age_hours = 24
purge_after_days = 30All values can be overridden with environment variables: SYNAPTO_PG_DSN, SYNAPTO_REDIS_URL, SYNAPTO_EMBEDDING_PROVIDER, SYNAPTO_EMBEDDING_MODEL, SYNAPTO_EMBEDDING_DEVICE, SYNAPTO_DEFAULT_TENANT.
SYNAPTO_DEFAULT_TENANT (and [defaults] tenant) is a fallback, not a pin: it is consulted only when the working directory has no usable git remote. Set it when the server runs somewhere that is not a checkout — a container, a CI job, a shared shell — and you want those writes to land in a named partition. Inside repositories the derived tenant wins, and an unset fallback resolves to default. Like an explicit tenant= argument, a non-canonical value here is rejected rather than repaired, and the error names the config source instead of blaming the caller.
Using as a Python library
from synapto.db.postgres import PostgresClient
from synapto.db.migrations import run_migrations, ensure_hnsw_index
from synapto.embeddings.registry import get_provider
from synapto.search.hybrid import hybrid_search
pg = PostgresClient("postgresql://localhost/synapto")
await pg.connect()
await run_migrations(pg)
provider = get_provider()
await ensure_hnsw_index(pg, provider.dimension)
results = await hybrid_search(pg, provider, "outbox pattern", tenant="myproject")
for r in results:
print(f"[{r.depth_layer}] trust={r.trust_score:.2f} {r.content}")Documentation
Compositional algebra, probe, reason, contradict | |
Feedback loop and contradiction workflow | |
Coordinate planning, implementation, and review across agents | |
Versioned SQL files with rollback | |
Setup and usage with Claude Code | |
Setup and usage with Cursor | |
Using Synapto as a LangGraph tool | |
Using Synapto with Agno agents | |
Golden set, ablation and layer sweep — what the ranker was measured to do | |
Version PR, preflight, dispatch, verify |
Development
git clone https://github.com/ramonlimaramos/synapto.git
cd synapto
uv sync --extra dev # or: python -m venv .venv && pip install -e ".[dev]"
uv run synapto init
uv run ruff check src/ tests/ scripts/CI runs ruff check, bandit, pip-audit, a wheel build verified by scripts/verify_wheel.py, and the test suite on Python 3.11, 3.12 and 3.13. A separate release preflight workflow runs scripts/preflight_release.py on every pull request: it checks release.yml and the GitHub release environment against the hardened pipeline, so a drift turns the next pull request red instead of failing on release day. Releases themselves are dispatched by hand from main; see RELEASING.md.
Three conventions are enforced by tests rather than review:
SQL lives in
synapto/sql/. Every statement is a static constant with%(name)sparameters, one module per owner; Python selects statements, it never composes them.tests/unit/test_sql_lives_in_the_sql_package.pywalks the AST of both sides.Migrations are inventoried. A new file under
synapto/_migrations/must be added toEXPECTEDintests/unit/test_migration_resources.pyandEXPECTED_MIGRATIONSinscripts/verify_wheel.pyin the same commit, and a migration is never renumbered once merged. See docs/migrations.md.The destructive surface is declared. Every constant in
synapto/sql/that deletes, soft-deletes, drops, truncates or moves rows between tenants must appear in theSURFACEsnapshot oftests/unit/test_destructive_surface.pywith its reach — MCP tool, CLI command or library-only — and the test modules that prove refusal, happy path and "the rest was not touched". A new destructive statement fails the suite until it is declared.
Running the tests
The PostgreSQL-backed tests are destructive — they roll migrations down (dropping and recreating columns) and truncate tables. They therefore refuse to run against anything but a disposable database, and they never read your production SYNAPTO_PG_DSN.
Create a throwaway database once:
createdb synapto_test
psql -d synapto_test -c "CREATE EXTENSION IF NOT EXISTS vector; CREATE EXTENSION IF NOT EXISTS pg_trgm;"Then point SYNAPTO_TEST_PG_DSN at it:
SYNAPTO_TEST_PG_DSN=postgresql://localhost/synapto_test uv run pytestTwo fail-closed rules protect your real data:
No DSN, no connection. With
SYNAPTO_TEST_PG_DSNunset, the database-backed tests are skipped — never silently pointed at a default.pytestalone still runs every test that does not need PostgreSQL.The database must be named
*_test. The suite asks the live connection forcurrent_database()and aborts before any destructive setup if the name does not end in_test. Parsing the DSN is not enough: a DSN can omit the database name, and service files can redirect it.
Retrieval eval
tests/eval is a golden-set harness that measures what recall actually returns, not just that filters apply. It seeds a synthetic corpus (tests/eval/corpus.toml, ~200 memories under tenant acme/eval) through remember, runs the cases in tests/eval/golden/<signal>.toml through hybrid_search, and reports MRR@10 and Recall@5 per signal — layer, trust, decay, hrr, scopes, metadata, general. The numbers are compared with the committed tests/eval/baseline.json; a move beyond ±0.02 in either direction fails, so a ranking change cannot merge without re-baselining in the same PR. It runs with the rest of the suite (same SYNAPTO_TEST_PG_DSN, same deterministic offline embeddings, no model download).
The baseline is a measurement of the current ranker, not a target: a case the ranker gets wrong today stays in the set with its low score.
To add a case, append a [[case]] to the signal's file (query, expected corpus key, optional scopes, metadata_filter, depth_layer), add any new memory to corpus.toml with a stable key, then re-baseline — the corpus digest recorded in the baseline changes, and the gate says so:
SYNAPTO_TEST_PG_DSN=postgresql://localhost/synapto_test SYNAPTO_EVAL_WRITE_BASELINE=1 uv run pytest tests/evalCommit the resulting baseline.json next to the change that moved it. Keep the corpus synthetic (acme/* only); a test rejects anything else.
The same harness drives a ranking-signal ablation: each signal (HRR leg, decay, trust, layer weight, vector leg, keyword leg) is switched off in turn — as a variant of the production SQL and a zeroed HRR leg, applied in the test process only — and the golden set is re-run, with a noise floor from five shuffled-insertion runs deciding what counts as a real move. It is opt-in because it takes tens of seconds and produces a document rather than a pass/fail:
SYNAPTO_TEST_PG_DSN=postgresql://localhost/synapto_test SYNAPTO_EVAL_ABLATION=1 uv run pytest tests/evalThe report lands in docs/eval/ablation.md; docs/eval/README.md holds the reading of the latest run and the follow-up issues it produced.
A layer-weight sweep uses the same harness to choose the depth-layer multipliers: ten spreads are run under full and no-hrr, a spread is admissible when every layer case ranks its target first, the best overall MRR@10 among admissible spreads wins, and a tie within the noise floor goes to the narrower. The current 1.3 / 1.25 / 1.0 / 0.7 came from it; change DEPTH_BOOST only with a new run committed next to it:
SYNAPTO_TEST_PG_DSN=postgresql://localhost/synapto_test SYNAPTO_EVAL_LAYER_SWEEP=1 uv run pytest tests/evalThe report lands in docs/eval/layer_sweep.md.
License
MIT
This server cannot be deployed
Maintenance
Related MCP Connectors
- memnodeOAuthdev.memnode
Persistent, inspectable memory for AI agents with lineage, correction, and a hosted MCP endpoint.
Shared, governed long-term memory for AI agents across tools and sessions via MCP and REST.
Persistent memory for AI agents — log and recall conversation context over MCP.
Persistent memory for AI agents across Claude, ChatGPT and any MCP client.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceGives AI agents persistent memory with semantic search, automatic extraction, and memory decay, accessible via MCP protocol.5MIT
- AlicenseAqualityDmaintenanceProvides persistent memory with semantic search for MCP-based AI agents, enabling them to store and recall information across sessions using vector embeddings.41MIT
- AlicenseCqualityBmaintenancePersistent semantic memory for MCP-compatible agents, enabling them to remember and recall text, audio, and documents across sessions.1055MIT
- AlicenseNot gradedqualityBmaintenanceProvides persistent semantic memory for AI agents via MCP, enabling them to remember, recall, list, update, and forget memories with vector-based similarity search.ISC