MCP server and tools
semanticastindexer mcp runs an MCP server over stdio,
exposing SAI’s semantic code search to agentic coding tools (Claude Code, Cursor, Windsurf,
Codex, and others). It is built on the official Rust MCP SDK (rmcp, Cargo feature mcp).
This page is the authoritative reference for the server and every tool it exposes. The
.agents/skills/sai/SKILL.md agent skill defers to this page for the canonical tool contracts.
For per-client wiring (Claude Code, Cursor, Windsurf, Codex, …) see MCP clients. For the exact JSON shapes each tool returns, see Output schemas.
Server behavior
- Read-only by default. Search exposes no index/upsert/flush tools. Only
sai_refreshandsai_syncwrite, and only when explicitly enabled (see below). - Backend, embedder, and collection come from
sai-cfg.yml. See Backends and embedders. - Backend built once at startup. The backend and embedder are constructed a single time and reused across every tool call.
- Tools are
sai_-prefixed so they stand apart from other MCP servers’ tools in the agent’s tool list.
Worker-thread model
The DuckDB backend is !Send/!Sync, but rmcp’s tool-handler futures must be Send. So
the backend lives on a dedicated worker thread, and the server holds only a Send + Sync
channel handle (BackendHandle). Each tool handler builds a request, sends it to the worker,
and awaits a oneshot reply — keeping handler futures Send while serializing all backend
access through the single-threaded connection. For the full rationale see
how it works.
Concurrency and the index lock
The server opens the DuckDB index once and holds it for the life of the connection —
read-only by default, or read-write under --allow-write. DuckDB enforces single-writer /
multi-reader access at the file level across processes, so while an MCP server is connected:
- A separate CLI
sync, full re-index, orflush(all open the database read-write) against the same.indexwill fail or block on the lock — even a read-only MCP connection blocks a CLI writer. - Two read-only consumers (e.g. an MCP server plus a CLI
duplicates/similar) coexist fine.
So the in-process write tools are the preferred way to keep the index fresh without shelling
out: call sai_sync (reconcile the git-changed set) or sai_refresh (specific paths)
on the running --allow-write server — they mutate through the same connection, so there is no
second handle and no lock contention. If you do need heavy CLI maintenance, disable/disconnect
the MCP server first (via your client’s MCP toggle), run the CLI command, then reconnect.
Threshold and limit resolution
Two resolution rules apply throughout the tools:
- Threshold resolution (per knob):
MCP tool arg > config value > built-in default. The config values come from thesimilarity:block ofsai-cfg.yml; the built-in defaults arefind_similar_min_score = 0.85,duplicate_min_score = 0.93,duplicate_min_cluster_size = 2, andtop_k = 10. These cutoffs are model-specific — tune them per embedding model. See Tuning similarity. - Limit clamping: any caller-supplied
limit/top_kis clamped to[1, 50]so a single call can’t request the world.
Tools
The server exposes seven tools. Four are read-only; sai_refresh and sai_sync write and
sai_prepare_mcp_setup can execute a setup script — all gated behind explicit flags.
| Tool | Purpose | Gating |
|---|---|---|
sai_search_code | General semantic search (query embedded as a query) | none (read-only) |
sai_find_similar | Neighbours of one snippet or one stored chunk | none (read-only) |
sai_find_duplicates | Codebase-wide near-duplicate clusters | none (read-only) |
sai_index_status | Index metadata (backend, model, dim, count, …) | none (read-only) |
sai_prepare_mcp_setup | Return setup commands; optionally run the setup script | execution requires --allow-setup |
sai_refresh | Re-index specific files in place (delete + re-embed) | requires --allow-write |
sai_sync | Reconcile the index with the working tree (git-changed set), like CLI sync | requires --allow-write |
Skills & subagents that drive these tools
The MCP server gives an agent the capability; two companion artifacts (both
installed by mcp-setup/setup.sh) give it the judgment to use the tools well:
sai-deslop(.agents/skills/sai-deslop/SKILL.md) — a portable Agent Skill (Claude Code, Cursor, Windsurf, …) covering when to call each tool while coding and a triage protocol that forces the agent to read each finding’s real source and classify it (real / boilerplate / intentional / fragment) before proposing a verified consolidation. It treats a cluster as a hypothesis, not a conclusion.dedup-auditor(.claude/agents/dedup-auditor.md) — a Claude Code subagent that runs the heavyweight repo-widesai_find_duplicatessweep and per-member reads in an isolated context, applies the same triage protocol, and returns a classified digest instead of a raw cluster dump.
Skills are the portable, behavior-shaping layer; the subagent is the
context-isolating worker for repo-wide audits. Both ultimately call the sai_
tools below.
sai_search_code
General semantic search over the indexed code. The query is embedded as a query and the
nearest indexed chunks are returned. The server over-fetches (about limit × 4, still clamped)
so the language / path_glob post-filters can still return up to limit rows. When
include_text is false, each snippet is capped to the first ~8 lines and ~800 chars.
| Arg | Type | Required | Default |
|---|---|---|---|
query | string | required | — |
limit | integer | optional | 8 (clamped to [1, 50]) |
language | string | optional | unset (no language filter; e.g. "ts") |
path_glob | string | optional | unset (e.g. "src/**") |
include_text | boolean | optional | false (return capped snippet) |
sai_find_similar
Find code similar to either an inline code snippet or an existing indexed chunk addressed
by path + line. Provide either code OR both path and line — not a mix:
codeis embedded as a passage (code-vs-code space) and requires a local embedder (theduckdbbackend); calling it against a non-local-embedding backend returns aninvalid_paramserror.path+linelooks up the exact stored vector for that chunk (no re-embed) and excludes the chunk itself from its own results. If no indexed chunk exists at that location the call returnsno indexed chunk at <path>:<line>.
min_score resolves as arg > config > built-in default 0.85. Omitting it still applies the
configured (model-tuned) cut; pass an explicit 0.0 to see the raw score distribution.
| Arg | Type | Required | Default |
|---|---|---|---|
code | string | one of code or path+line | unset |
path | string | use together with line | unset |
line | integer | 1-based start line; use with path | unset |
limit | integer | optional | 8 (clamped to [1, 50]) |
min_score | number | optional | config find_similar_min_score, else 0.85 |
sai_find_duplicates
Find near-duplicate clusters across the index. For each chunk it takes the chunk’s top_k
nearest neighbours, keeps the edges whose similarity is >= min_score, and unions them into
clusters via union-find. Clusters with size >= min_cluster_size are returned, largest first.
| Arg | Type | Required | Default |
|---|---|---|---|
min_score | number | optional | config duplicate_min_score, else 0.93 |
min_cluster_size | integer | optional | config duplicate_min_cluster_size (else 2), then floored at max(…, 1) |
path_glob | string | optional | unset (restrict the scan to matching paths) |
max_clusters | integer | optional | 50 (local constant; not configurable via similarity:) |
top_k | integer | optional | config top_k (else 10), clamped to [1, 50] |
Note: min_score, min_cluster_size, and top_k follow arg > config > built-in default,
while max_clusters is a fixed local default of 50 and has no config knob.
sai_index_status
Report index metadata for freshness and sanity checks. Takes no arguments. Returns the backend, collection, embedding model, vector dimension, total chunk count, and chunker.
| Arg | Type | Required | Default |
|---|---|---|---|
| (none) | — | — | — |
sai_prepare_mcp_setup
Help an agent set up SAI as an MCP server for a project. By default it only returns the
exact commands and an MCP config snippet to run; it executes the setup script only when
execute: true and the server was started with --allow-setup. Without --allow-setup,
an execute: true call is reported as blocked (Server not started with --allow-setup) and no
script runs.
The recommended_command includes --target-dir and an explicitly-derived --features list.
For a prebuilt/release binary — where mcp-setup/setup.sh is not on disk beside it — the
command falls back to the install.sh one-liner instead, and an execute: true call is reported
as blocked (building from source needs a source checkout).
| Arg | Type | Required | Default |
|---|---|---|---|
target_directory | string | optional | current working directory |
backend | string | optional | "duckdb" (or "qdrant") |
embedder | string | optional | "ollama" (or "ort" for fully offline) |
use_ast_chunker | boolean | optional | false (requires a binary built with --features ast) |
install_globally | boolean | optional | false (installs into ~/.local/bin as a sai wrapper) |
execute | boolean | optional | false (only runs the script when also started with --allow-setup) |
The response includes the recommended setup command, an mcp_server_config_example, and a
numbered next_steps list. The first build can take several minutes (much longer for ort).
sai_refresh
Write tool. Re-index specific files in place: for each path it deletes that path’s existing
points, then re-chunks, re-embeds, and re-upserts the files that still exist and pass the index
filters (extension, globs, not generated). Paths that are gone or excluded are removed.
The whole batch runs in one bulk window (HNSW drop → per-path delete + re-embed + upsert →
rebuild), reusing the same per-file logic as the sync command.
| Arg | Type | Required | Default |
|---|---|---|---|
paths | array of string | required | — (non-empty; max 200 per call) |
Gating: the server must be started with --allow-write. Without it the backend is opened
read-only and any call returns:
server is read-only; restart with --allow-write to enable refresh
An empty paths array returns refresh requires at least one path; more than 200 paths
returns too many paths (max 200). On success the tool returns the refreshed paths (with
chunk counts) and the removed paths.
sai_sync
Write tool. Reconcile the index with the working tree, the MCP analog of the CLI sync
command: it resolves the git-changed file set and runs them through the same per-file reconcile
as sai_refresh (survivors re-chunked/re-embedded, deleted or now-excluded paths removed).
| Arg | Type | Required | Default |
|---|---|---|---|
since | string | optional | HEAD~1 (changed set = working tree vs <since>) |
staged | boolean | optional | false (use git diff --cached instead of --since) |
paths | array of string | optional | — (explicit set; overrides git detection) |
Gating: like sai_refresh, requires --allow-write (else server is read-only; restart with --allow-write to enable sync). When git finds nothing the tool returns an empty result with a
note; more than 200 changed files returns an error suggesting a narrower since or explicit
paths. On success it returns the same {refreshed, removed} shape as sai_refresh.
Wiring (.mcp.json)
Point command at the built binary (an absolute path is safest) and set cwd to the indexed
project root so the server finds that project’s index and sai-cfg.yml:
{
"mcpServers": {
"sai": {
"command": "/path/to/semanticastindexer/target/release/semanticastindexer",
"args": ["mcp", "--config", "sai-cfg.yml"],
"cwd": "/path/to/your/project"
}
}
}
Build with the needed features and index the project once before starting the server. To enable
the write tool, add --allow-write to args; to allow sai_prepare_mcp_setup to execute its
script, add --allow-setup:
{
"mcpServers": {
"sai": {
"command": "/path/to/semanticastindexer/target/release/semanticastindexer",
"args": ["mcp", "--config", "sai-cfg.yml", "--allow-write"],
"cwd": "/path/to/your/project"
}
}
}
See MCP clients for per-client config locations and the
CLI reference for the full mcp flag list. For the response shape of
each tool, see Output schemas.