Local-first Cognitive Memory Operating System + Model Context Protocol (MCP) server for autonomous AI coding agents.
WrongSynapse is a zero-cloud memory layer that replaces a plain vector database with tri-hybrid retrieval:
- Semantic — local ONNX embeddings (
@huggingface/transformersv4,Xenova/all-MiniLM-L6-v2, 384-dim, mean-pooled, L2-normalized). - Lexical — SQLite FTS5 full-text index with BM25 ranking.
- Structural — a hierarchical knowledge graph linking projects, monorepo packages, directories, files, AST symbols, git commits, and memories.
The three channels are fused with Reciprocal Rank Fusion (RRF, k = 60) into high-precision agent context. Everything runs 100% locally. No cloud APIs, no telemetry.
| Node.js | ≥ 22 (22 and 24 tested in CI; node:sqlite fallback needs ≥ 22.5) |
| OS | Windows, Linux, macOS (CI: ubuntu-latest + windows-latest; native modules exercised on both) |
| Disk | ≈ 25 MB for the embedding model (q8 ONNX — downloaded automatically on first use, then offline) |
| git | optional — only for --git history linking |
When wrongsynapse starts, it also boots a single-page admin panel on a
kernel-assigned local port (different on every startup). The UI is built
with React 19 + Tailwind CSS v4 + Radix UI Themes 3 + React Flow 12,
uses a dark-pro design palette, and lives entirely in the same Node
process as the MCP server — no second port, no second process.
Four tabs cover the surface the MCP tools expose:
| Tab | What it shows |
|---|---|
| Search | Tri-hybrid retrieval in the browser — the same engine behind the synapse_hybrid_query MCP tool (FTS5/BM25 + semantic embeddings + graph expansion, fused with RRF). Tune channel weights, graph depth, limit, and a scope prefix; every hit shows its fused score (with a bar relative to the top result) and the per-channel ranks (FTS #1, VEC #2, GRAPH —) that produced it, plus warnings when the semantic channel is unavailable |
| Statistics | Entity / relation / vector / candidate / FTS-row counts, plus a per-type breakdown of entities and relations |
| Memory | Searchable list of every memory_entry (filter by scope and/or free-text); click a row for the full detail panel (scope, name, content, metadata, anchored-to graph paths, confidence, updated timestamp); remove with a confirmation dialog |
| Graph | React Flow visualisation: every memory_entry plus the non-memory endpoints of every relation that touches one. Pan, zoom, fit-to-view. |
The UI is opt-out, not opt-in — it boots by default. Disable it with
--no-web or SYNAPSE_WEB=0. Bind to a specific port with
--web-port <n> or SYNAPSE_WEB_PORT=<n> (default: kernel-assigned free
port, so every startup uses a different port). Auto-open the browser
with --web-open or SYNAPSE_WEB_OPEN=1 (best-effort; silently no-ops
on hosts without a default browser).
# Default behaviour — MCP + admin UI
wrongsynapse
# WrongSynapse admin web UI listening on http://127.0.0.1:50935
# Disable the web UI
wrongsynapse --no-web
# Pin the web UI to a specific port
wrongsynapse --web-port 9090
# WrongSynapse admin web UI listening on http://127.0.0.1:9090The web server binds to 127.0.0.1 only and never governs process
lifetime (httpServer.unref()); a web-bind failure logs a warning and
the MCP server continues unaffected. Every request must carry a loopback
Host header (127.0.0.1 / localhost / [::1], any port) — anything
else is rejected with 403, which closes the DNS-rebinding route of reading
the corpus from a remote website via the user's own browser. Destructive
operations (DELETE /api/memory/:id) additionally require an
Authorization: Bearer <token> header where the token is a random hex
string generated at every boot and printed to stderr on the line after the
listen URL (admin web UI delete token: …) — read-only endpoints (health,
stats, search, list, graph) are open. Responses carry baseline hardening
headers (X-Content-Type-Options: nosniff; the SPA shell additionally gets
X-Frame-Options: DENY and a Content-Security-Policy).
The SPA is built separately from the Node bundle so the published npm package stays lean. From a checkout:
npm run web:build # one-shot: install web deps, typecheck, vite build → web/dist/
npm run web:dev # dev server on :5173 with /api proxying to localhost:8765
npm run web:typecheck # tsc -b --noEmit on the SPA onlynpm run web:install runs npm --prefix web install for CI to pre-populate
web/node_modules/ before the typecheck/build steps. The build artifact
(web/dist/) is git-ignored but IS shipped: the npm-published tarball
contains dist/ (the Node server bundle), web/dist/ (the SPA),
skills/, and CHANGELOG.md.
The web server's REST surface (used by the SPA and by anyone scripting against the admin panel):
| Method | Path | Returns |
|---|---|---|
GET |
/api/health |
{ ok, version } |
GET |
/api/stats |
counts + type/relation breakdowns |
GET |
/api/search?q=…&scope=…&type=…&limit=…&vector_weight=…&lexical_weight=…&graph_weight=…&graph_depth=… |
tri-hybrid RRF results: { results[{ score, ranks{fts,vector,graph}, entity, graph_paths }], warnings, vector_retrieval_used } |
GET |
/api/memory?scope=…&q=…&limit=… |
{ count, memories[] } (scope and q compose) |
GET |
/api/memory/:id |
one memory + its anchored graph paths |
DELETE |
/api/memory/:id |
{ id, deleted: true } (Bearer auth required) |
GET |
/api/candidates?status=…&limit=… |
{ count, candidates[] } |
GET |
/api/graph/memory?limit=… |
{ nodes[], edges[] } (React Flow shape) |
# install the published package
npm install -g wrongsynapse
# index your workspace to populate ./synapse.db — on the first run the
# embedding model (≈ 25 MB) downloads automatically into ~/.cache/wrongsynapse;
# after that one-time download everything runs offline
wrongsynapse --index .
# start the MCP server (stdio by default)
wrongsynapseStrict-offline hosts can forbid the download entirely: pass
--no-remote-model (or set SYNAPSE_NO_REMOTE_MODEL=1) and WrongSynapse
loads the model from the cache only — SYNAPSE_MODEL_DIR can point at a
pre-provisioned model directory. SYNAPSE_ALLOW_REMOTE_MODEL=1 keeps the
older always-allow semantics for remote fetches.
Prefer not to install globally? npx wrongsynapse --index . works the same (the one-time model download is cached per machine), or build from source:
git clone https://github.com/ersinkoc/WrongSynapse.git
cd WrongSynapse && npm install && npm run build
node dist/index.js --index . # same CLI, from the repoThe --index . run prints JSON stats (files scanned/parsed, symbols, relations, embeddings) and creates ./synapse.db.
WrongSynapse speaks the Model Context Protocol over stdio or SSE. Point any MCP client at the installed CLI:
Claude Desktop (claude_desktop_config.json):
{
"mcpServers": {
"wrongsynapse": {
"command": "wrongsynapse",
"env": { "SYNAPSE_DB_PATH": "D:/path/to/synapse.db" }
}
}
}Cursor / any stdio MCP client — command wrongsynapse (installed globally or via npx wrongsynapse), then the thirteen synapse_* tools appear.
SSE (remote / concurrent clients):
wrongsynapse --transport sse --port 8765
# endpoint: http://localhost:8765/sse (binds 127.0.0.1 by default)
# Explicitly expose to the LAN (no auth on this transport — trusted networks only):
wrongsynapse --transport sse --port 8765 --sse-host 0.0.0.0Each SSE connection gets its own MCP session — multiple agents can query one shared database concurrently. The SSE transport exposes every MCP tool with no authentication, so it binds to loopback unless you opt in with --sse-host (env: SYNAPSE_SSE_HOST).
WrongSynapse ships a skills.sh-compatible agent skill that automates the memory workflow (recall before work → observe during work → consolidate after). With any supported agent (Claude Code, Cursor, Codex, Copilot, and 20+ more):
npx skills add ersinkoc/WrongSynapseThis drops skills/wrongsynapse/SKILL.md into your agent's skill directory. The skill instructs the agent to use the synapse_* MCP tools — configure the MCP server as shown above (command: wrongsynapse) so both halves are connected. To use the skill straight from a checkout instead, copy skills/wrongsynapse/SKILL.md into your agent's skills folder.
| Tool | Purpose |
|---|---|
synapse_index_workspace |
Scan a workspace; parse structure + AST symbols (TS/TSX/JS/Py/Go/Rust via tree-sitter); embed; optionally link git commits; persist the hierarchy. |
synapse_hybrid_query |
Tri-hybrid RRF retrieval with scope/type filters, per-channel weight tuning (vector/lexical/graph, graph_depth), and contextual graph paths. |
synapse_anchor_memory |
Store a decision/convention note anchored to a symbol, file, package, or project scope (with embedding + ANCHORED_TO edge). |
synapse_graph_neighbors |
Traverse the relational sub-graph (callers/callees, anchored memories, hierarchy, commits); accepts an entity id or an exact scope path. |
synapse_record_observation |
Write an uncommitted insight into the episodic candidate pool (does not touch the graph yet). |
synapse_promote_candidate |
Promote a pending candidate into a permanent memory entity: embedding computed, ANCHORED_TO link created. |
synapse_list_candidates |
List the candidate pool, filterable by lifecycle status (pending / promoted / discarded). |
synapse_discard_candidate |
Discard a pending candidate — terminal: a discarded candidate can never be promoted. |
synapse_remember |
Store a durable memory (fact/convention/decision/warning/anti-pattern) with kind, importance, TTL, tags, and cosine-≥0.85 auto-deduplication into a SUPERSEDES chain. |
synapse_recall |
Retrieve memories filtered by kind, importance, tags, or scope prefix; updates last_accessed_at (access tracking). |
synapse_memory_search |
Tri-hybrid search over all entities filtered by memory_kind/type, with per-channel ranks and graph paths. |
synapse_link_memories |
Link two memory entries with a SUPERSEDES relation (version chains, corrections). |
synapse_purge_expired |
Delete all TTL-expired memory entries. |
synapse_record_observation
│
▼
[ pending ] ──── synapse_discard_candidate ──▶ [ discarded ] (terminal)
│
synapse_promote_candidate
│
▼
[ memory_entry ] (embedded + anchored to a scope)
The candidate pool is the write buffer for episodic memory: observations accumulate cheaply, then an agent (or human) promotes the keepers and discards noise.
--demo (or npm run demo) continuously streams synthetic observations into memory and periodically consolidates them — promoting high-confidence keepers into permanent memory_entry entities (with embeddings and ANCHORED_TO graph edges) and discarding noise. It is the fastest way to watch the full memory lifecycle live in the admin web UI: candidates pile up in the pool, keepers get promoted, and the Graph tab grows real anchor edges.
npm run demo # 1 observation/second, seeded 42
npx wrongsynapse --demo --demo-interval 250 --demo-seed 7
SYNAPSE_DEMO=1 SYNAPSE_DEMO_INTERVAL=5000 wrongsynapse- Isolated by default — demo mode writes to
./synapse-demo.db, never your realsynapse.db(pass--dbto override deliberately). - Reproducible — content, scope rotation, and confidence all derive from the seeded PRNG (
--demo-seed); the same seed replays the same stream. - Namespaced — every demo entity lives under
proj:demo/..., so demo data can be swept with one scope-prefix delete and can never interleave with real project scopes. - Observes the MCP semantics — the feeder mirrors
synapse_record_observation→synapse_promote_candidate/synapse_discard_candidate, including graceful degradation when the embedding model is unavailable (promoted without a vector).
wrongsynapse [options]
--transport <stdio|sse> MCP transport (default: stdio)
--port <number> SSE HTTP port (default: 8765, env SYNAPSE_PORT)
--sse-host <host> SSE bind address (default: 127.0.0.1, env SYNAPSE_SSE_HOST;
the SSE transport is unauthenticated — only expose on
trusted networks)
--db <path> SQLite database path (default ./synapse.db, env SYNAPSE_DB_PATH)
--model-dir <dir> Local embedding model directory (env SYNAPSE_MODEL_DIR)
--allow-remote-model Always allow remote model fetches (env SYNAPSE_ALLOW_REMOTE_MODEL=1)
--no-remote-model Strict offline: cache-only model loads, never download
(env SYNAPSE_NO_REMOTE_MODEL=1)
--index <workspace> One-shot: index a workspace and exit (prints JSON stats)
--git With --index: also link git commit history
-h, --help Show help
-v, --version Show version
The database is closed cleanly on SIGINT/SIGTERM (server modes) and immediately after a one-shot --index.
| Variable | Purpose | Default |
|---|---|---|
SYNAPSE_DB_PATH |
SQLite database path | ./synapse.db |
SYNAPSE_MODEL_DIR |
Directory with local model files (offline inference) | transformers cache |
SYNAPSE_EMBEDDING_MODEL |
Embedding model id | Xenova/all-MiniLM-L6-v2 |
SYNAPSE_ALLOW_REMOTE_MODEL |
1 to always allow remote model fetches (the default already downloads once automatically) |
unset (auto) |
SYNAPSE_NO_REMOTE_MODEL |
1 for strict offline — never download, cache-only |
unset (auto) |
SYNAPSE_PORT |
SSE HTTP port when --port is absent |
8765 |
SYNAPSE_SSE_HOST |
SSE bind address when --sse-host is absent |
127.0.0.1 |
Every entity is addressed by a hierarchical scope URI:
proj:app/pkg:core/dir:src/file:auth.ts/sym:validateToken
proj:backend/file:src/auth/Token.ts # file values may contain '/'
proj:app/commit:9f2a3b1c… # git commits
- Pair separator is
/; kind/value separator is:. - Prefixes filter retrieval:
scopes: ["proj:app/file:src/auth"]matches that file and everything below it. - Prefix matching is boundary-aware:
proj:appmatchesproj:app/…but never the siblingproj:app2/…(enforced in SQL, index-friendly).
| Table | Holds |
|---|---|
entities |
The unified node store: project, package, directory, file, symbol, commit, memory_entry (+ candidates in their own table). Scope path, name, content, JSON metadata, confidence. |
relations |
Graph edges: CONTAINS, CALLS, ANCHORED_TO, INTRODUCED_BY_COMMIT (unique per source/target/relation, FK-cascaded). |
entities_fts |
FTS5 external-content index over name/content/scope_path, auto-synced by triggers. |
entity_vectors |
One Float32 embedding BLOB per entity (384-dim), FK-cascaded. |
memory_candidates |
The episodic pool: content, scope, source, confidence, lifecycle status. |
Schema migrations are versioned (PRAGMA user_version) and transactional; an FTS5 capability probe runs at boot.
The package doubles as a library (ESM, full type declarations):
import {
openDatabase, migrate, // SQLite bootstrap (better-sqlite3 → node:sqlite fallback)
insertEntity, insertRelation, // graph writes
insertCandidate, listCandidates, // episodic pool
createEmbedder, // local embedding engine
hybridSearch, // tri-hybrid RRF retrieval
parseScope, scopeMatchesAnyPrefix, // scope URI helpers
dbStats,
DemoFeeder, // continuous synthetic ingestion (demo mode)
} from 'wrongsynapse';
const db = await openDatabase('./synapse.db');
migrate(db);
const embedder = createEmbedder(); // loads the model lazily on first use
const candidateId = insertCandidate(db, {
content: 'AuthService caches tokens for 5 minutes',
scopePath: 'proj:app/file:src/auth.ts',
confidence: 0.85,
});
const { results } = await hybridSearch(db, embedder, {
query: 'token caching',
scopes: ['proj:app/file:src'],
limit: 5,
});
// DemoFeeder drives the same lifecycle programmatically (returns a Promise —
// await feeder.stop() before closing the DB; it waits out any in-flight tick).
const feeder = new DemoFeeder({ db, embedder, intervalMs: 1000, seed: 42 });
feeder.start();
// ... later:
await feeder.stop();
db.close();npm run typecheck # strict tsc, zero errors
npm test # 376 tests
npm run test:coverage # 100% statement/branch/function/line gate (enforced in CI)
npm run build # tsc -> dist/ (ESM + .d.ts)
npm publish --dry-run # prepublishOnly guard runs typecheck + test + build firstCI (.github/workflows/ci.yml) runs on push/PR: Node 22 + 24 on Linux (typecheck, tests with the 100% coverage gate, build, npm audit failing on high/critical, plus the web SPA typecheck + build) and a Windows job (typecheck, tests, build) that exercises the native modules on the platform the project is developed on.
src/
├── index.ts # CLI entry point & MCP server bootstrap (+ library exports)
├── db/
│ ├── connection.ts # SQLite driver abstraction (better-sqlite3 → node:sqlite), WAL pragmas
│ ├── schema.ts # Versioned migrations (PRAGMA user_version), FTS5 probe
│ └── queries.ts # Entities, graph traversal (BFS), FTS5/BM25, vector I/O, candidates
├── engine/
│ ├── embedding.ts # transformers.js v4 singleton (all-MiniLM-L6-v2), offline env config
│ ├── vector-math.ts # Cosine similarity, L2 normalization, mean pooling, BLOB (de)serialization
│ ├── parser.ts # Tree-sitter workspace indexer: gitignore-aware walker, symbols, edges, embeddings
│ ├── git.ts # simple-git wrapper: commits, per-commit diffs, blame
│ └── hybrid-search.ts # Tri-hybrid retrieval + Reciprocal Rank Fusion (k = 60)
├── mcp/
│ ├── server.ts # McpServer bootstrap: stdio + SSE transports (concurrent SSE sessions)
│ └── tools/ # 8 MCP tool definitions + registration
└── utils/
└── scope.ts # Scope URI grammar: parse, build, boundary-aware prefix matching
- Model weights live in the local transformers cache or
SYNAPSE_MODEL_DIR. - The only network WrongSynapse ever performs is the one-time, automatic
download of the embedding model (≈ 25 MB from the HuggingFace CDN) when the
cache is empty — afterwards every load is local and the process returns to
local-only mode. Set
SYNAPSE_NO_REMOTE_MODEL=1(or--no-remote-model) to forbid even that: remote fetching is then refused at the transformers.js env level. - If the model is missing and cannot be downloaded, indexing and querying degrade gracefully: structural + lexical retrieval keep working; semantic retrieval is skipped with a warning.
npm auditis clean at the high/critical level (CI-enforced).package.jsoncarriesoverrides(protobufjs,sharp,adm-zip) pinning patched versions of transitive dependencies from the ONNX runtime stack required by@huggingface/transformers.
See CHANGELOG.md.
MIT © Ersin KOÇ