Home/API

Triple API surface

Reach the same engine three ways.

Read a file, call a syscall, or hit the network — the query semantics, and the results, are identical. On the web tier REST, gRPC and MCP all pass through one App::dispatch, so a Search over gRPC returns exactly what /search does.

In plain terms There are three different ways to reach SynaFS, and all three give the exact same answers. You can treat it like an ordinary folder (everyday tools such as ls, cat and grep just work), talk to it like a device built into the system, or reach it over the network — including MCP, the protocol AI assistants use to call tools. Pick whichever fits the tool you already have.
NATIVE · FUSE

Filesystem

Mount the repo passthrough (hot reads ~0 overhead) with a virtual /.syna/** namespace. cat, ls, grep, getfattr just work.

$ syna mount /mnt/repo
$ ls /mnt/repo/.syna/\
     symbol/validate_token/callers
SYSCALL · libsyna

Device & C ABI

/dev/synafs (CUSE) with a write-query / read-NDJSON model, plus libsyna.so for any FFI caller. io_uring batch.

echo '{"text":"parse config"}' \
  > /dev/synafs
cat /dev/synafs   # NDJSON hits
WEB · MCP / gRPC / REST

Network

REST + WebSocket, gRPC-Web and native HTTP/2 (hand-rolled HPACK), MCP for agents. One App::dispatch; TLS/mTLS opt-in.

$ syna serve --addr :5200   # REST
$ syna grpc  --addr :5201   # HTTP/2
$ syna-mcp .                # agents
A

Native — FUSE magic paths

A pure-Rust fuser mount (no libfuse — default-features=false, mounted via fusermount3) lays a passthrough over the backing repo: everything outside /.syna/** is byte-for-byte the underlying filesystem, and hot reads are forwarded with near-zero overhead (FUSE passthrough / FOPEN_PASSTHROUGH for direct kernel I/O). Only the /.syna prefix is intercepted; SynaFS logic lives in a pure, unit-tested resolver.

The virtual namespace turns queries into directories you can list, read, and pipe through ordinary tools:

/.syna/query/<q>/
URL-encoded query (filters included) → a directory of hit entries; each entry reads back as a # synafs hit view with path / lines / score headers then the span.
/.syna/symbol/<name>/{def,refs,callers,callees}
Symbol-graph relations as files; read for the symbol's location view.
/.syna/similar/<path>
Semantic neighbours as symlinks back to the real files (target passthrough).
/.syna/history/<path>/<rev>
The as-of blob content of that path at that revision.
/.syna/ctl
Write commands (commit, reindex, sync) to trigger engine actions.

Results are lazy: nothing runs until readdir, and they're cached by (query, generation) on a short TTL so repeated stat storms are cheap. Each backing file also carries virtual xattrs — user.syna.embedding (base64 vector), user.syna.symbols (JSON), user.syna.summary (optional LLM summary) and user.syna.generation — readable with getfattr -d and read-only.

$ syna mount /mnt/repo
# a query is just a directory; ls runs it lazily
$ ls /mnt/repo/.syna/query/parse%20config%3F--lang%3Drust/
000_src_config_rs_L12.view   001_src_cli_rs_L88.view  ...
$ cat /mnt/repo/.syna/query/parse%20config%3F--lang%3Drust/000_src_config_rs_L12.view
# synafs hit  ·  path: src/config.rs  ·  lines: 12-40  ·  score: 0.031
$ getfattr -d -m user.syna /mnt/repo/src/config.rs
B

Syscall — device & C ABI

New syscalls would need a kernel patch, so the engine is exposed as a /dev/synafs CUSE character device (userspace char device — the same wire protocol as FUSE, implemented directly over /dev/cuse, no libfuse). Variable-length JSON moves over write/read instead of the ioctl-retry dance: write(query json) runs the query, read drains the results as NDJSON (one hit per line + a trailing summary line).

Three ioctls, numbered _IOWR('S', nr, …), switch a fd's mode: SYNA_QUERY (1), SYNA_OPEN_SEMANTIC (2, route the next read to the last query's best match), SYNA_SUBSCRIBE (3, turn the fd into a reindex-event NDJSON long-poll, same meaning as the web /events).

For any C / C++ / FFI caller there is libsyna.so with a small, stable JSON-in / JSON-out C ABI (struct marshalling avoided on purpose):

// libsyna.h — JSON in / JSON out
typedef void *syna_handle;

syna_handle syna_open(const char *repo_path);                       // NULL on error
int  syna_query(syna_handle h, const char *json, char **out);       // 0=OK, *out=malloc'd JSON
int  syna_query_batch(syna_handle h, const char *json_arr, char **out); // N queries, one crossing
void syna_free(char *s);                                            // free *out
void syna_close(syna_handle h);

syna_query_batch runs N queries in a single boundary crossing (submit-N / complete-N), isolating per-slot failures as {"error":code}. The kernel-side counterpart is the --features io_uring batch: write_read_batch chains each item's write(query)→read(result) with IOSQE_IO_LINK so a whole batch is submitted and reaped in one io_uring cycle — pure-Rust io-uring (raw io_uring_setup/enter, no liburing). Device creation needs CAP_SYS_ADMIN, but the protocol state machine, dispatch and NDJSON codec are pure functions and are unit-tested without a device.

C

Web — MCP / REST / WS / gRPC

MCP is the agent front door: syna-mcp speaks line-delimited JSON-RPC 2.0 over stdio with tools search, read_span, neighbors, symbol_lookup, impact, path, diff_impact, context_pack, file_info, apply_edit, subscribe, and diff_symbol. Register it with Claude Code or any MCP client (command = syna-mcp, args = [repo]).

REST (tiny_http, synchronous, loopback bind + Bearer token) exposes the same engine over HTTP/JSON:

GET /healthz
Liveness + generation (the only route that skips auth).
POST /search · /read_span · /symbol · /neighbors
Hybrid search, exact spans, symbol relations, semantic neighbours — read-your-writes via consistency:strong + ryw_token.
POST /apply_edit · /commit · /diff · /history
Edit (returns a token), commit, symbol diff and version history.
GET /events · /ws
Reindex events as a long-poll, or a WebSocket (RFC 6455, hand-rolled sha1/base64) text-frame stream.

gRPC comes two ways: gRPC-Web (application/grpc-web+proto) over plain HTTP/1.1, and native HTTP/2 (h2c) with a hand-rolled HPACK (Huffman included) — both with no async runtime and no protoc. Search, ApplyEdit, Commit and Health route through REST's same App::dispatch (protobuf → Value → dispatch → Value → protobuf), so a gRPC Search hit equals the REST /search hit. TLS/mTLS is opt-in via rustls (client certs without a chain to the CA are rejected at the handshake, before dispatch). Ports: 5200 (web) / 5201 (gRPC).

Agent tools

The twelve MCP tools, and what each replaces

Every tool is designed around one question: how many turns does the agent need? A grep loop answers a structural question by reading one file per hop and burning a turn each time. These resolve the same question against the index — the call graph, the version DAG, the vector index — and return the whole answer in a single call.

In plain terms An AI coding assistant can only do what its tools let it do. Given just a shell, it explores a codebase the way you would with no editor — guess a filename, search for a word, open the file, guess again. Each of those guesses is a round-trip that costs time and money. The twelve tools below replace whole loops of guessing with a single question the index can answer outright.
ToolWhat it answersInstead of
search“Where is X handled?” — hybrid vector + BM25, RRF-fused. The top hit carries its full source inline, so the answer usually needs no follow-up read. Pass texts: [...] to answer several independent questions in one call.a grep loop that opens one file per guess
impactnew“What breaks if I change this?” — the transitive closure of callers, grouped by hop distance. The blast radius before you touch a shared helper. direction=callees gives the dependency set instead.one grep + read per node, per hop
pathnew“How does A reach B?” — the shortest chain(s) of direct calls between two functions, every step citing its definition site. If nothing is found, the call may flow the other way.hand-running a directed grep hop by hop
context_packnew“Explain this function.” — its full body, the bodies of what it directly calls (how it works), and its callers as one-line references (who depends on it). budget_bytes caps the payload.a read_span + grep sequence per helper
diff_impactnew“What did this change touch, and which tests should run?” — the entity-level diff between two versions (default: uncommitted work), every symbol transitively affected via callers, and which of those are tests.git diff → read files to find enclosing functions → grep callers per symbol
symbol_lookupResolve a symbol by name: def, refs/callers, callees, importers. Edges resolve same-file-first, so a homonym in another module doesn't hijack the answer.grepping for call sites and filtering by eye
read_spanRead an exact span by chunk_id, or by path with a line range. If a search already inlined that span, the response says so instead of paying for it twice.cat + manual line counting
neighborsChunks semantically nearest a given path or chunk — “what else looks like this?”guessing at filenames
apply_editWrite a file (full content or byte-range splices) and enqueue a transactional reindex. Returns a token; pass it as a later search's ryw_token to read your own write.write, then hope the index caught up
diff_symbolDiff a single symbol's source between two revspecs (HEAD~3, a version hex, working) — the function's history, not the file's.git log -p on the whole file
file_infonewIs this file vector-indexed, and if not, why — binary, oversize, or untracked. Tells an asset apart from source before you trust a search miss.assuming a search miss means the code isn't there
subscribeReindex events as they happen — long-poll or WebSocket, for an agent that wants to react to the tree changing under it.polling the filesystem
Freshness is not the agent's problem. Every one of these reads reconciles out-of-band edits before it answers, so an agent that writes with its own tools — not apply_edit — still never searches a stale index. SYNA_FRESH=0 opts out and SYNA_FRESH_MAX caps the sweep (default 64); as_of history queries never reconcile, since the past does not drift.
D

Auto-reindex — any surface, always fresh

Because every surface shares one write path, a write from any of them — a FUSE passthrough write, apply_edit over REST/MCP, or an external editor — triggers reindex. syna watch closes the loop with two kernel fsnotify backends: inotify (the default, unprivileged, runs in sandboxes/CI), and fanotify (--fanotify, raw fanotify_init/fanotify_mark syscalls, FAN_MARK_MOUNT to cover a whole mount, needs CAP_SYS_ADMIN). A per-path content-hash guard stops the reindex write-back from echoing, and .syna/dotfiles are skipped.

One engine, identical results. REST, gRPC and MCP go through the same App::dispatch; FUSE, the device, and libsyna run the same syna-query model. A query means the same thing — and returns the same hits — no matter which surface you reach it through.