Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

API and configuration reference

Everything the server exposes: the REST API served on web_address (default 127.0.0.1:8080), the gRPC service on address (default 127.0.0.1:50051), the server’s command line, and the configuration file. The web UI’s own /docs.html page carries the same REST reference with live examples.

REST API

Errors are JSON ({"error": "…"}) with the right status: 400 for bad parameters or an invalid regex, 404 for an unknown file, 503 with Retry-After while the index is being written, 504 with the same envelope when the request timeout is hit.

EndpointDescription
GET /api/searchSearch (text, regex, symbols, references)
GET /api/file?file=…Full content of an indexed file
GET /api/context?file=…&line=N&context=KLines around a match (K ≤ 200)
GET /api/dependents?file=… / GET /api/dependencies?file=…Files that import this file / files it imports
POST /mcpModel Context Protocol for coding agents. See MCP
GET /api/graph/…The import graph: neighbourhoods, impact, import chains, the folder map. See Import graph
GET /api/statsIndex statistics (files, trigrams, dependency edges, content bytes)
GET /api/statusIndexing progress, and how current the index is (see Index freshness)
GET /api/healthLiveness ({"status":"healthy","version":…,"problems":N}); problems counts startup and storage problems
GET /api/readyReadiness: 200 once there is an index to search, 503 otherwise
GET /api/diagnosticsIndex health, extension breakdown, self-tests, and service: web UI and gRPC status, where the index is saved, and the problems (a problem marks the status degraded)
GET /metricsPrometheus text format (request counters, latency histogram, index gauges)
WS /ws/progressLive indexing progress frames

GET /api/search

ParameterDefaultDescription
qrequiredQuery. Plain text understands the query syntax; with regex=true it is a regular expression.
max50Page size, 1 to search.max_page_size (1,000) (0 = search.default_page_size).
offset0Hits to skip; ordering is deterministic, so offset=max is page two. At most search.max_page_offset (100,000 by default).
regexfalseTreat q as a regex (Rust regex syntax, matched one line at a time unless the pattern mentions \n or sets (?s)).
symbolsfalseDefinitions only (functions, types, classes, …) plus filename matches.
referencesfalseUses of the identifier in q: call sites and type mentions. Cannot be combined with regex or symbols (400).
case—true / false overrides case: in the query. Plain text ignores case by default; a regex is case-sensitive by default and case=false adds (?i).
word—true for whole-word matching.
include / exclude—Semicolon-separated path globs (src/**/*.rs;lib/**).
rankautoEvery mode reads candidate files best first (by a per-file score that needs no reads) until the match budget is spent. auto and fast also stop at max_files when one applies; full never does.
max_filessearch.fast_rank_files (no limit)Most candidate files a search reads. Capped by search.max_fast_rank_files, reported in max_files_allowed when the server sets one. When candidates_searched < total_candidates, ask again with max_files=total_candidates to search the rest.
context0Context lines before and after each hit, 0 to search.max_context_lines (10).
timeout_ms0Stop scanning after this long and return the best hits so far (capped at search.max_timeout_secs, 30 s; every search also runs under the server’s request timeout).
files0Also return up to this many files whose path matches q, 0 to search.max_path_matches (100), in files (first page of a text or regex search only).

Response:

{
  "results": [{
    "file_path": "proj/src/main.rs",
    "line_number": 5,
    "content": "fn helper_target(x: i32) -> i32 {",
    "match_start": 3, "match_end": 16,
    "line_match_start": 3, "line_match_end": 16, "match_column": 3,
    "content_truncated": false,
    "score": 12.5,
    "match_type": "SYMBOL_DEFINITION",
    "dependency_count": 2,
    "file_hits": 3,
    "context_lines": ["…"], "context_start_line": 4
  }],
  "query": "helper_target",
  "total_results": 1, "offset": 0, "total_matches": 3,
  "has_more": true, "truncated_by_budget": false,
  "elapsed_ms": 0.7, "rank_mode": "full",
  "total_candidates": 12, "candidates_searched": 12,
  "index": { "watching": false, "synced_at_ms": 1791789240000 }
}

match_start/match_end are byte offsets into content (which may be a truncated window of a long line, see content_truncated); line_match_start/line_match_end are byte offsets into the full line and match_column the 0-based character column, for placing an editor cursor. line_number is 0 for a filename match. match_type is TEXT, SYMBOL_DEFINITION or SYMBOL_REFERENCE. file_hits is how many hits the search found in the result’s file across all pages (a file cut at a page boundary shows only some of them on this page); file_hits_capped: true means the file reached the per-file cap and may hold more. To fetch one file’s hits, repeat the search with include set to that path (glob metacharacters escaped as [*]). total_matches is present when the search ran to completion; otherwise truncated_by_budget is true and has_more means more may exist. index says how current the index is; see Index freshness.

With files=N, two more fields list the files whose path matches, best first (file name hits above directory hits, then shorter paths). They are separate from results, which are unchanged:

{
  "files": [{ "file_path": "proj/src/web/api.rs", "score": 8.0, "match_ranges": [[5, 19]] }],
  "total_files": 1
}

match_ranges are [start, end) byte offsets into file_path, one per word of the query (path matching splits it on spaces). Every word must appear in the shown path (case-insensitive unless case:yes, \ read as /); a word containing / may also match the absolute path, so a pasted absolute path finds its file. file:, lang:, include and exclude filter as for content. See how matching compares with grep and ripgrep.

Index freshness

With watch = false (the default is true), the server indexes the tree when it starts and does not see files written afterwards. Search then misses text written after that scan, while /api/file and MCP read_file read the disk and already show it. Every search response, /api/status and the /ws/progress frames carry index so clients can say so:

FieldMeaning
watchingA file watcher applies edits, renames and deletes as they happen.
synced_at_msWhen the last full scan of the indexed folders started (Unix ms). Every change made before then is in the index. Absent until the first build or reconcile finishes.
updated_at_msWhen the watcher last changed the index (Unix ms).

GET /api/status?paths=src/a.rs;src/new.rs also compares each path (at most 100, separated by ;, as results print them, relative to an indexed folder, or absolute) with the disk and returns "paths": [{"path": …, "state": …}]. state is one of the following:

StateMeaning
currentThe file’s modification time and size match what was indexed.
staleThe file changed after it was indexed.
deletedStill in the index but gone from disk.
not_indexedOn disk but not in the index: written after the last sync, or excluded by the configuration.
not_foundNeither in the index nor under an indexed folder.
unknownIndexed, but its indexed version was not recorded.

When watching is false, the web UI shows “Index synced 6 min ago · watching off” beside the search time, fcs prints the same warning on stderr, and MCP search_code ends with it. Updating the index on request (for example right after an agent writes a file) is tracked in #140.

Query syntax

Plain-text queries (not regex=true) are a literal, like rg -F: the query minus its operators is searched for as written.

SyntaxMeaning
fn mainThe phrase fn main, exactly one space. Several words are one phrase.
"fn main", -WallQuotes and a leading - are part of the text.
file:PATTERN / -file:PATTERNOnly / never paths matching the glob; a bare word matches anywhere in the path, src/ means everything under src.
lang:rust / -lang:pyOnly / never files of that language.
case:yesCase-sensitive.
word:yesWhole words only.

Ranking

Each hit’s score combines the match with structural signals (weights in src/search/ranking.rs):

SignalEffect
Line defines a symbol3×
Exact case2×
Match at the start of the line1.5×
File under src/ or lib/1.5×
Files that import this file1 + 0.5·log10(dependents)
Long lineinverse-length damping
Test or example path (fast mode)0.7×

Results are in rank order with each file’s hits kept together: files in the order of their best hit, then every hit of that file by score.

Import graph

The dependency explorer (/graph.html) is built on these. Every endpoint answers from memory under the engine read lock. file accepts a display path (project/src/main.rs) or any path /api/file accepts. Unknown files are 404.

Rust mod foo; declarations are edges from a parent module to its own child. They describe structure, not dependencies, and would put every parent and child in a cycle, so they are left out unless containment=true (also accepted by every endpoint below). A pub use child::X re-export is the same edge, so it is left out too.

EndpointParametersReturns
GET /api/graph/neighborhoodfile, depth (1–4, default 2), limit (files per level, default 12, max 500), expand (comma-separated levels to list up to 500 files, e.g. -2,1)nodes (path, signed depth: negative for files it imports, positive for files importing it; imports, imported_by, test, cycle), edges among them (from, to, cycle, containment), hidden (per level: count and the shared folder or number of folders), and upstream / downstream totals
GET /api/graph/impactfile, limit, expandEvery file that transitively imports file, by distance: affected, levels, tests_total, tests (up to 200, nearest first), plus nodes / edges / hidden as above
GET /api/graph/pathfrom, toThe shortest import chain as files (each imports the next). When there is none from from to to, the reverse chain with reversed: true; found: false when neither exists
GET /api/graph/modules—The graph collapsed to folders: folders (files, imports, imported_by, cycle) and edges (from, to, count of file imports, cycle); unresolved_imports, the imports that resolve to no indexed file, and unresolved_local, those of them written as a path into the project (./x, .x, crate::, @/), which point at a broken import or a form fcs does not resolve. Cached until the index changes
GET /api/graph/importsfileThe file’s import statements: line (1-based), spec as written, the indexed target it resolves to (null for packages, the standard library and files outside the index), containment
GET /api/graph/filesq (case-insensitive substring), limit (default 100, max 1000)files most connected first, with imports / imported_by, and the total that matched

Imports are resolved for Rust (crate::, self::, super::, sibling modules and the crate’s own name), Python and JavaScript/TypeScript.

MCP (coding agents)

The web server also speaks the Model Context Protocol at /mcp, so a coding agent can use the index instead of grepping: search, read files with line numbers, and walk the import graph. Point the agent at the URL:

claude mcp add --transport http fast_code_search http://127.0.0.1:8080/mcp

For clients that only launch local commands, fcs mcp relays MCP over stdio to the same endpoint (it finds the server like every fcs command: --server, $FCS_SERVER, the configuration, then http://127.0.0.1:8080):

{ "mcpServers": { "fast_code_search": { "command": "fcs", "args": ["mcp"] } } }
ToolArgumentsWhat it returns
search_codequery (the query syntax), mode (text, regex, symbols, references), include / exclude globs, limit (default 50, max 1,000: mcp.default_limit / mcp.max_limit), offset (page on from there), max_files, rank, context (0–5)path:line:column: line per hit, with the match count and, when more exist, the offset of the next page; in text and regex mode up to 20 files whose path matches are listed first (mcp.file_matches)
read_filepath, start_line, end_lineNumbered lines, 200 by default and at most 1,000 (mcp.read_file_lines / mcp.max_read_file_lines), with how to continue
file_dependenciespath, direction (in, out, both), depth (1–4), limitFiles it imports and files importing it, by distance, with the line of each direct import
change_impactpaths (one or more), limitEvery file that transitively imports any of them, nearest first, split into tests and other code
import_pathfrom, toThe shortest chain of imports between two files, either way round
dependency_overviewdirectory (optional), limitThe most connected folders, the heaviest folder-to-folder imports, folder cycles and hub files
index_statuspaths (optional, at most 100)When the index last synced with disk and whether file watching is on, plus, for each path, whether it is current, stale, not indexed yet or deleted. See Index freshness

Every tool is read-only and answers from the same handlers as the REST API, so it shares their search permits, deadlines and caps. Results use the root-relative paths search results print, and lists are capped with a note saying what was left out. The graph tools say which languages have import edges (Rust, Python, JavaScript/TypeScript), so an empty answer for another language is not mistaken for “unused”.

Transport details: each JSON-RPC message is a POST /mcp answered with application/json (notifications get 202); there is no event stream (GET /mcp is 405) and no session. Requests carrying a browser Origin are refused with 403 unless it is a loopback address, the server’s own address, or listed in cors_origins, so a web page cannot drive a local server through the visitor’s browser. Like the REST API, /mcp has no authentication: anyone who can reach the web port can read the index.

gRPC

The schema is proto/search.proto: Search streams SearchResult messages with the same fields and modes as REST (is_regex, symbols_only, references, case_sensitive, whole_word, include_paths, exclude_paths, rank, offset, deadline_ms), and Index adds paths under the configured roots. The standard grpc.health.v1 service is served too. A minimal client: examples/client.rs (cargo run --example client).

Limits match REST: offset ≤ max_page_offset, fast ranking opens fast_rank_files, the search semaphore is shared with the REST side, and every search runs under a deadline derived from the request timeout.

Server command line

fast_code_search_server [OPTIONS]

  -c, --config <FILE>       Configuration file
  -a, --address <ADDR>      gRPC listen address (overrides config)
      --web-address <ADDR>  Web UI / REST listen address (overrides config)
  -i, --index <PATH>        Additional path to index (repeatable)
      --no-auto-index       Skip indexing on startup
      --no-grpc             Do not start the gRPC API (same as enable_grpc = false)
      --init <FILE>         Write a documented template configuration and exit
      --static-dir <DIR>    Serve the UI from disk (development)
  -v, --verbose             Debug logging

Configuration discovery: --config, then $FCS_CONFIG, then ./fast_code_search.toml, then ~/.config/fast_code_search/config.toml. The first log lines say which file was used (or that none was found and the defaults apply) and any warnings about it.

If one of the ports cannot be bound, the server logs which port and keeps running on the other API; it exits only when neither could start. The diagnostics page and /api/health report the failure.

Configuration file

fast_code_search_server --init config.toml writes a fully commented template. The keys that matter most:

[server]
address = "127.0.0.1:50051"       # gRPC
enable_grpc = true                # false (or --no-grpc) for web UI / REST / fcs only
web_address = "127.0.0.1:8080"    # REST + web UI
enable_web_ui = true
max_concurrent_searches = 64      # shared by REST and gRPC; beyond it: 503 / RESOURCE_EXHAUSTED
request_timeout_secs = 30         # searches get an engine deadline just under this
fast_rank_files = 2000            # files fast ranking opens by default (0 = all)
max_fast_rank_files = 0           # most a request may ask for with max_files (0 = no limit)
max_page_offset = 100000          # deepest offset a client may page to (0 = no limit)
cors_origins = []                 # e.g. ["https://ide.example.com"]; empty = same-origin only

[indexer]
paths = ["~/work", "/srv/repos/other"]     # ~ expands; relative paths resolve against this file
exclude_patterns = ["**/node_modules/**", "**/target/**", "**/.git/**"]
include_extensions = []           # empty = every text file; ["rs", "py"] to restrict
max_file_size = 10485760          # bytes; 0 = default
respect_gitignore = true
show_root_name = true             # false: src/main.rs instead of myapp/src/main.rs (one path only)

index_path = "~/.local/share/fast_code_search/index.fcsidx"   # persist across restarts
save_after_build = true
checkpoint_interval_files = 20000
watch = true                      # follow edits, renames, deletes
save_after_updates = 100          # save after this many watched file changes

--init writes every key with its default, and sets index_path from the template’s own name (--init work.toml gives ~/.local/share/fast_code_search/work.fcsidx). Without index_path the index lives only in memory and is rebuilt on every start; the startup log warns about it.

Only one server writes an index. A server holds a lock on <index_path>.lock and records its PID and addresses in <index_path>.lock.owner; a second server given the same index_path logs who owns it, loads the index read-only and never saves. Give each configuration its own index_path.

At startup and before each save the server checks free space and free inodes on the filesystem holding the index (and inodes where indexed source lives), and warns when either is low.

Changing exclude patterns, extensions, the size cap or a .gitignore takes effect on the next start: files that are no longer eligible are dropped when the persisted index is reconciled.

Exit codes and errors (CLI)

fcs exits 0 when it printed at least one match, 1 when the search found nothing, and 2 on error (bad arguments, invalid regex, server error, no server and no index). See CLI.md.