syn mcp starts a Model Context Protocol server over stdio. Any MCP client can plug in. We document Claude Code below; the wiring is the same for any client that supports stdio MCP servers.
What gets exposed
Twenty-five tools. Twelve read-only on the current workspace (incl. query, the read-only aggregation tool, SYN-183, and workspace_health, SYN-310); four read-only across opt-in workspaces (SYN-78, SYN-92 read slice); one maintenance tool (reindex, SYN-138); one audit tool (replay_proposal, client-driven: it returns replay material the agent re-runs; no server-side sampling, SYN-219); one integrity tool (verify, SYN-263: proves the memory was not silently corrupted); six write proposals routed through a human confirmation queue. Write tools never touch the workspace directly: the AI proposes; you sanction (or, under the safe auto-apply policy, low-risk reversible writes apply and are stamped auto-accepted; SYN-218).
The read contract (SYN-177/178/201/203). Every read tool returns the
uniform { "result": <payload>, "freshness"? } envelope as MCP
structuredContent (read that field directly: no prose parsing),
plus a compact-JSON text mirror for clients without structured
support. Each read tool advertises an outputSchema on tools/list,
and search_knowledge hits + get_entity attach a resource_link to
synapse://entity/{type}/{id} so a result is openable with resources/ read: no second get_entity round-trip.
Read-time hints (SYN-308). The high-value reads (search_knowledge, search_everything, workspace_overview, list_by_type, recent_changes, blame) attach a deterministic _meta sidecar, a sibling of structuredContent, so it never touches the outputSchema contract. It carries workspace (which brain answered), a precision breakdown of the SYN-304 relevance bands ({strong, good, partial}) and result_count for searches, and a next_step, the cheapest sensible follow-up (triage-then-get_entity, paginate with cursor, call workspace_health, propose an update on a low-trust blame). The agent reads _meta to judge whether results are on-topic before spending tokens, and errors carry a matching data.remediation (SYN-309, below).
Workspace-local
| Tool | Purpose | Inputs |
|---|---|---|
search_knowledge | Search the whole workspace. Hybrid (FTS + vector + graph, RRF) by default; degrades to plain FTS5 when the semantic models aren’t downloaded yet (or on a minimal core build). | query, optional type/tag/owner/limit/mode/as_of |
get_entity | Returns the frontmatter + body of one entity as {"markdown": "<full text>"}. | type, id |
get_entities | Batch read (SYN-181): many entities in one call, each optionally projected to chosen fields (fields: ["title","status","body"]) instead of full markdown. A missing/mistyped item yields a per-item error. Capped at 100. | items: [{type, id, fields?}] |
list_by_type | Lists entities of one type with optional status filter. Bounded + paginated (SYN-176): a full page means more remain. | type, optional status, limit (default 200), cursor (offset), as_of (valid-time date, SYN-223) |
project_graph | Inbound + outbound link neighbours of an entity. | type, id, optional rel |
workspace_overview | One-call digest (SYN-179): per-type counts, total, and the most-recently-updated entities: orient without one list_by_type per type. | optional recent (default 10, max 50) |
workspace_health | Agent-facing health (SYN-310): recent activity + hygiene (pending reviews, orphans, dangling, untagged) + a deterministic next_steps list, each with the exact syn command to hand the human. MCP counterpart of syn digest. | optional days (default 7) |
recent_changes | “What changed since I last looked” since an RFC-3339 instant. Returns decisions (audit log at/after since, newest first: SYN-180) and deltas (per touched entity, the field-level before/after with AI authorship: {slug, type, author, accepted_at, changes:[{field, kind, before, after}]}: SYN-240, git-derived against the commit that predates since). CLI counterpart: syn changes (SYN-256). | since, optional limit (caps decisions; default 100, max 500) |
blame | AI provenance lineage of an entity, who/what touched it, when, on whose behalf, with the deterministic trust score (SYN-240/256). MCP counterpart of syn blame. | slug |
diff | Per-field before/after of an entity over git since a ref (default HEAD); provenance keys excluded (SYN-240/256). MCP counterpart of syn diff. null when not a git repo. | slug, optional since |
query | Read-only aggregation (SYN-183, ADR-023): count entities by a whitelisted dimension, optionally filtered: compute totals out of context without paging entities. Takes no SQL; reads only entities/tags. Returns {group_by, buckets:[{value,count}], total}. | group_by (type|status|owner|priority|due|tag), optional filter (type/status/owner/priority/tag), limit (1–1000) |
verify | Prove the AI memory was not silently corrupted (SYN-263 / ADR-035): runs the integrity invariants (reindex fixpoint, audit payloads, history chain + checkpoint, supersession close) and returns {ok, report}, a signed synapse.verify-report/1. ok=false ⇒ an invariant was violated. deep adds INV-4 (governed-journal replay integrity, SYN-266 / ADR-036) + INV-13 (deterministic logical-contradiction sweep: overlap/cycle/inverted interval, guards the un-gated direct-edit path, SYN-316); rebuild lists every diverging fact on an INV-1 mismatch (SYN-269). Offline; tamper-evident, not tamper-proof. MCP counterpart of syn verify. | optional deep, rebuild (bool) |
search_knowledge mode (v0.5, SYN-88). Mirrors the CLI search
modes: "hybrid" (default, FTS + vector + graph fused with RRF),
"semantic" (vector similarity only), or "no-vector" (FTS + graph,
skips the embedding step). The semantic layer ships in the default
server (SYN-168); the vector legs go quiet: every search falls back to
plain FTS5: when the models haven’t been downloaded yet or on a minimal
core build (--no-default-features --features mcp). The hits keep the
{type, id, title, snippet} shape (plus matched_by/relevance/compatibility
when fused: SYN-304/306), wrapped in the {result, freshness?}
envelope described above; a client that does not set mode keeps the
default hybrid ranking. The independent legs (FTS + vector) run in parallel on
separate connections (SYN-306 / ADR-044); the fused order is a pure function of
the per-leg orders, so it stays deterministic. There is no separate semantic_search tool, the mode parameter delivers it without duplicating the search path.
Enrichment on accept (v0.5, BR-13). With the semantic models present,
accepting a write proposal (or any other workspace write) runs the
insertion-time enrichment pipeline inside the transaction: the new entity
is embedded into entities_vec and its mentions are linked, so it is
immediately findable by the next search_knowledge call. NER mentions
that don’t resolve queue their own create_entity_from_ner proposals for
review. Before the models are downloaded, the write still succeeds, enrichment is skipped gracefully and never triggers a download (run syn embed --all to backfill once the models are in place).
Federation (v0.2, SYN-78 + SYN-92 read slice)
| Tool | Purpose | Inputs |
|---|---|---|
list_workspaces | Every workspace the user opted in for cross-workspace queries. Identity fields (description, topics, tags, language) come from each workspace’s SYNAPSE.md. | none |
federated_search | Keyword search (FTS + graph, no per-workspace vector leg) fanned out across opt-in workspaces. Each workspace’s hits carry the same search_knowledge hit shape. Returns [{workspace, id, hits}]. | query, optional type/tag/owner/limit/workspaces |
get_entity_from | Read one entity from a specific opt-in workspace. | workspace, type, id |
get_workspace_manifest | Read the SYNAPSE.md identity card of an opt-in workspace. | workspace |
Write proposals (v0.2, SYN-60/61/62/92)
Every write tool below enqueues a proposal; it does not touch the workspace. The response carries the pending_id plus accept_command / reject_command strings the LLM can surface verbatim. The user reviews with syn pending show <id> and applies with syn pending accept <id>. See BR-12.3: MCP não escreve sem confirmação humana.
| Tool | Purpose | Inputs |
|---|---|---|
capture_fact | Propose creating a new entity (note, decision, person, project, …). | type, title, optional body/tags/fields/reason |
link_entity | Propose linking two entities with a labelled relation. | from_type, from_id, to_type, to_id, rel, optional reason |
update_status | Propose updating an entity’s status field (task done, project paused). | type, id, status, optional reason |
add_workspace_topic | Propose appending a topic to an opt-in workspace’s SYNAPSE.md. | workspace, topic, optional reason |
add_workspace_tag | Propose appending a tag to an opt-in workspace’s SYNAPSE.md. | workspace, tag, optional reason |
propose_batch | Bundle several proposals into ONE pending row the human accepts with a single syn pending accept (SYN-182). Each item is {kind, payload} where kind ∈ capture_fact/link_entity/update_status/topic_add/tag_add (note: topic_add/tag_add, the leaf-kind names) and payload is that tool’s usual fields, validated up front. Applied in order on accept (best-effort sequential, not one atomic transaction: atomicity tracked as SYN-202). | proposals: [{kind, payload}], optional reason/context |
Every write tool also accepts an optional context JSON snapshot (SYN-114) recorded with the proposal for syn audit replay.
Response shape for every write tool:
{
"pending_id": 7,
"accept_command": "syn pending accept 7",
"reject_command": "syn pending reject 7",
"kind": "capture_fact",
"summary": "note \"PKCE OAuth decision\""
}
Write tools return this object as structuredContent plus a compact-JSON text mirror (the read envelope is for read tools only). The same use cases back the CLI; there is no “second truth”.
Freshness advisory (SYN-93, ADR-011)
Every tool response that touched a workspace can carry a freshness block when the workspace’s tracked origin branch differs from the local one (or the check has timed out: Phase 2). The shape is:
{
"result": <original tool payload>,
"freshness": {
"workspace": "client-alfa",
"status": "diverged",
"behind_count": -1,
"last_check": "2026-05-25T14:00:00Z",
"advice": "git -C /Users/.../client-alfa pull"
}
}
Rules:
resultis always present (the uniform envelope, SYN-203); only thefreshnessblock is conditional; it is attached when there is something to say, and omitted forup_to_date/unknownstatuses (silence is the polite default).behind_count = -1in Phase 1 means “histories differ; the precise count lands with SYN-93b”.adviceis always a concretegit -C <root> pullcommand the LLM can surface verbatim.- The check itself is filesystem-only and rate-limited (default 30 min); the AI never pays a network round-trip for it.
Disable per-workspace with syn ws freshness off <name>. Disable globally with [freshness] enabled = false in any config layer. The MCP enrichment ignores silent_cli; that knob only governs the CLI stderr line.
Privacy gate (BR-12.4, SYN-79)
Federation defaults OFF. A workspace only appears in list_workspaces, contributes hits to federated_search, or is readable via get_entity_from / get_workspace_manifest after the user runs syn ws include <name> on it.
This is a structural invariant, not a check: every fan-out path in the codebase goes through crate::core::federation::fan_out{,_filtered}, which hard-codes Registry::list_federated() (the SQL filter is WHERE federated = 1). Non-opt-in ids passed to federated_search.workspaces are silently dropped: distinguishing “exists but not federated” from “does not exist” would leak the bit BR-12.4 is meant to keep private. The same applies to get_entity_from and get_workspace_manifest: both raise an indistinguishable entity '<name>' not found for “no such workspace” and “exists but private”.
Claude Code
The zero-friction path (SYN-288): let syn write the config for you:
syn mcp install # Claude Desktop (claude_desktop_config.json)
syn mcp install --client claude-code # project-local .mcp.json in the current dir
syn mcp install --workspace my-brain # pin a brain by name/path (SYN-300 / #287)
syn mcp install --print # print the JSON snippet, write nothing
syn mcp uninstall # remove just the synapse entry
--workspace <path|name> (SYN-300 / #287). By default the installed config
runs syn mcp with no cwd, so it only finds a brain when the client launches
inside the workspace. Pass --workspace and install bakes --workspace <ref>
into the config args, so the server serves that brain from any directory, the cross-project “query my brain from another repo” flow, with no manual editing.
install merges a synapse entry into the existing config: other MCP
servers and unrelated keys are left untouched, the prior file is backed up to
*.json.bak, and re-running updates the entry in place (idempotent). It points
command at the absolute path of the running syn binary.
Prefer to edit by hand? Add this to ~/.claude.json (or run
claude mcp add synapse …):
{
"mcpServers": {
"synapse": {
"command": "syn",
"args": ["mcp"],
"cwd": "/Users/you/brain"
}
}
}
commandis whateversynyou installed (which synto confirm).cwdis the workspace you want the server to read from.- No environment variables needed for v0.1.
Serve a brain from another project (cross-directory)
syn mcp reads the workspace from the current directory by default, so a client
launched in a different project (not itself a Synapse workspace) has nothing to
serve. To point the server at your brain wherever you are, pass --workspace
(SYN-300 P1): an absolute path, or a registered name/id from the global
catalog (syn ws list; every syn init registers there):
# by path
claude mcp add synapse -- syn mcp --workspace /Users/you/brain
# or by registered name/id: works from any project directory
claude mcp add synapse -- syn mcp --workspace my-brain
Now Claude Code in any repo answers from that brain: you don’t have to cd
into it. (This is cleaner than the cwd field above, which depends on the client
honoring a working directory; --workspace resolves the brain itself.)
Restart Claude Code and ask, in any project:
“Who is the focal contact for projeto-alfa, and which APIs do they own?”
Claude calls project_graph(type=project, id=projeto-alfa) to find Maria, then get_entity(type=person, id=maria-silva) to read her file, then surfaces the answer with the right slugs.
Other MCP clients
If your client speaks MCP over stdio, the launch command is the same:
syn mcp
Synapse also advertises resources on resources/list: the workspace manifest, the server card, synapse://capabilities (SYN-311, the self-describing vocabulary: entity types + their fields, link relations, reserved x-* keys, write-proposal payload shapes, enrichment sources; read it before proposing to avoid an invalid kind/rel/key, the same dump the CLI syn context prints), every entity, and the recent audit trail.
Synapse advertises tool schemas (input + output JSON Schema) on tools/list, each with MCP tool annotations (readOnlyHint on every read, no destructiveHint on the additive write proposals, idempotentHint on reindex) so a client can auto-allow reads and gate writes without guessing. The 25 names are search_knowledge, search_everything (SYN-226, RRF over every kind), query (SYN-183, read-only aggregation), get_entity, get_entities, list_by_type, project_graph, workspace_overview, workspace_health (SYN-310), recent_changes, blame (SYN-256), diff (SYN-256), verify (SYN-263, proves non-corruption), list_workspaces, federated_search, get_entity_from, get_workspace_manifest, reindex (SYN-138, emits Tasks progress under --features mcp_tasks), replay_proposal (SYN-136/SYN-219, client-driven: it returns replay material the agent re-runs: no server-initiated sampling, which the MCP spec has since deprecated in SEP-2577), capture_fact, link_entity, update_status, add_workspace_topic, add_workspace_tag, propose_batch.
Actionable errors (SYN-309)
A recoverable condition (unknown id/type, a missing/invalid proposal field, no workspace, a model still downloading) comes back as a JSON-RPC invalid_params error whose data carries a stable code (the i18n key), a category (user/transient), recoverable/retryable flags, and a remediation string naming the exact next step (e.g. “unknown type: read the synapse://capabilities resource for the registered types”, or “no such entity: discover ids with search_knowledge”). The remediation is also appended to the human-readable message. Genuine faults (a corrupted audit trail, a migration mismatch) stay internal_error but are still annotated, so a client never has to parse prose to know whether the agent can retry. Internal paths and SQL are never leaked (ADR-023 posture).
Locking down what the AI sees
The server’s cwd workspace is always fully readable through the local read tools (search_knowledge, search_everything, get_entity, get_entities, list_by_type, project_graph, workspace_overview, workspace_health, recent_changes, plus the read-only query aggregation). Other workspaces are only reachable when the user has opted them in:
cd ~/work-brain && syn ws include work-brain # join the federated pool
cd ~/client-acme-brain && syn ws include client-acme-brain
syn ws exclude work-brain # leave anytime
The federation tools (list_workspaces, federated_search, get_entity_from, get_workspace_manifest) only ever see workspaces marked federated = 1. Workspaces left at the default (private) never leak through the server, even if the LLM literally asks for them by name. See the Privacy gate section above.
The simplest segmentation is still one workspace per audience:
~/brain/ ← personal: notes, decisions, drafts
~/work-brain/ ← work: APIs, stakeholders, tasks
~/client-acme-brain/ ← per-client
Run one syn mcp per cwd (Claude Code spawns one process per mcpServers entry); opt only the workspaces you trust into the federated pool.
Inspect the server by hand
Useful when something is off. The protocol is JSON-RPC over stdio:
cd ~/brain
{
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"manual","version":"0"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
'{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"search_knowledge","arguments":{"query":"pagamentos"}}}'
sleep 1
} | syn mcp | jq .
You’ll see serverInfo + the full tools/list schema set (25 tools, each read carrying an outputSchema) + a search_knowledge result whose structuredContent is the {result, freshness?} envelope, with resource links on the hits. With the semantic models present the result is hybrid-ranked; add "mode":"semantic" to the arguments for vector-only.
Shipped in v0.5
| Capability | Ticket |
|---|---|
Hybrid ranking in search_knowledge + the mode parameter (FTS + vector + graph, RRF) | SYN-86 / SYN-88 |
| Insertion-time enrichment on accepted proposals (embed + NER + link) | SYN-160 / SYN-161 |
Network ls-refs freshness (precise behind_count, unreachable fallback) | SYN-93b |
Prompts
Nine named prompts (the synapse: namespace), each a deterministic function of (name, args, workspace): no LLM round-trip to assemble:
synapse:capture-decision: guidedcapture_factflow for a decision note.synapse:daily-review: yesterday’s accepted proposals + open tasks.synapse:weekly-brief: audience-tailored weekly briefing.synapse:proposal-contract(SYN-252): the proposal-quality contract an agent should read before a capture session.synapse:onboard-workspace(SYN-312): first-run orientation: read the capabilities + overview, confirm the self-identity, learn the propose→accept gate. Start a fresh session here.synapse:orient(SYN-312): the cheapest-read decision table (which read tool for which question) as a fetchable prompt.synapse:catch-me-up(SYN-312): resume with a delta viarecent_changessince a given instant, not a full re-read.synapse:resolve-contradiction(SYN-312): when acapture_factreturns acontradictssignal, walk the supersede-don’t-duplicate path (x-supersedes).synapse:capture-and-verify(SYN-312): the full governed-write loop: propose → hand over the accept command → offersyn verify.
Contradiction detection (SYN-242)
At propose-time Synapse checks, deterministically and on structured fields, whether a write contradicts an accepted fact: same subject + predicate, different object: an update_status that flips a field to a different value, or a link_entity that re-points the same edge to a different relationship. A contradiction is never “low-risk reversible”, so it is forced to the human gate even under auto_apply = safe, and the write tool’s response carries a contradicts: {subject, predicate, existing, proposed} signal. Resolve by superseding the existing fact (x-supersedes, SYN-223): invalidation, not duplication, not by adding a near-duplicate. Free-text (NLI) contradiction and same-subject detection across separate captures (identity resolution) are deferred to v0.11 (SYN-245/254).
Proposal-quality contract (SYN-252)
The differentiator of governed memory is that a human accepts each fact, so the scarce resource is the reviewer’s attention. A good proposal respects:
- Atomicity: one subject–predicate–object per proposal; never bundle unrelated claims.
- Observation date:
x-valid-fromis when the fact became true in the world, not when you learned it (transaction-time comes from git; don’t duplicate it). - No echo: search first; propose deltas, not duplicates of what’s already there.
- Supersede, don’t duplicate: a changed fact carries
x-supersedes: <old-id>(the old interval is closed on accept: invalidation, not deletion, SYN-223). - No meta-extraction: facts about the domain, not about the conversation.
- Attribution: cite the source; provenance (
x-ai-*) records who/when automatically. - Typed over prose: prefer a typed entity + indexed fields when the domain has structure (
syn types).
Agents fetch the canonical copy via prompts/get synapse:proposal-contract; this section is the human-readable mirror.
What doesn’t exist yet (and when)
| Capability | Ticket | Phase |
|---|---|---|
| HTTP/SSE transport (today: stdio only) | - | backlog |
When syn mcp hands back a (no hits) response, that’s Lil Syn dozing on an empty index. The fix is usually one of: a missing tag, a typo, or just not enough indexed yet. Run syn search <term> from your shell to see if you find it before blaming the AI.