MCP reference
Tool arguments, resource responses, and failure behavior.
This reference defines Rift's MCP tools and resources, including optional fields, paging, limits, and failures. For an introduction through an agent's workflow, see the MCP guide.
The expandable tool examples come from the served JSON schema. A result example on this page shows one answer in two tabs: its compact text and its JSON. Result content defines the compact text.
Map
rift://map reads a workspace orientation snapshot: a starting picture for a caller new to the
codebase. The answer carries:
revision- an eight-characterDigestof the indexed tree the map was computed fromlanguages- per-language file and symbol counts, sorted by language spellingmodules- the directory tree indexed files sit under, each entry's file and symbol counts inclusive of its descendants, folded three directories deephubs- the most-referenced symbols, ranked by reference count descendingentry_points- symbols where execution startsdocs- markdown files, in path ordermodule_relationships- the modules a resolved reference reaches between, ranked by that count descendingpackages- the packages the workspace's manifests and lockfiles state, inmanager,name, then selector orderpagination- always page zero of one, the whole map on one pagewarnings- conditions that qualify the answer, omitted when empty
During local preparation, the map carries local_index_preparing. File counts and
documentation paths can describe discovered files before their declarations are prepared;
symbol counts describe prepared declarations. The server MUST return the map's revision,
counts, and warnings from the same publication.
entry_points lists symbols carrying the entrypoint
SymbolFacet - a file-scope fn main, not one
nested in a module, an impl block, or another function. It stays empty for a workspace with no
such facts.
hubs ranks SymbolIds by how often the index
holds a resolved reference to them, and module_relationships folds both ends of every such
reference to the module modules lists them under, drops a pair whose two ends fold to one
module, and counts what is left. module_relationships carries at most 100 entries, ordered
by references descending, then from, then to, so a workspace past that bound keeps the
strongest pairs and two reads of one tree agree. Both read the index alone: the language
engine resolves a reference for one read and publishes none, so both lists stay empty until a
provider publishes resolved references into the index.
packages lists the dependency context: what the
Cargo, uv, npm, and Bun resolvers read from Cargo.toml and Cargo.lock, pyproject.toml and
uv.lock, package.json and package-lock.json, and package.json and bun.lock. The
resolvers read those files and run no toolchain. Each entry is a
PackageContextEntry carrying the exact
version a lockfile pins or the requirement a manifest declares, never both, and the
availability a global package index could answer under. The list follows those files: one
rewritten while the server runs is read again on the next publication.
A resolver reads at most 256 manifests per workspace, taken in path order, so packages over a
workspace holding more lists what the manifests it read declare.
packages also lists one entry for each standard library a language the workspace uses relies
on, under the manager stdlib: stdlib/rust, stdlib/node beside the npm typescript package,
or stdlib/python. Its version comes from the workspace's pins, or from a version probe under
[dependencies] resolution = "auto", and the
dependency context lists where each one is read.
A read of rift://map returns one application/json content under either
output selection:
{
"uri": "rift://map"
}Search
search
Searches indexed declarations and source lines by lexical `query`, merged with full-text matches from included `[search.text]` files and declaration bodies, and by a bounded relationship `traversal` from one seed symbol: `incoming` reaches the declarations referencing it, `outgoing` the declarations it calls. `pattern` matches a regex against the text of every indexed file, line by line as ripgrep reads it, and answers each match and each declaration holding one, in place of `query` and `traversal`. `change` answers the declarations a committed revision and another revision, or the working tree, hold differently, in place of `query` and `traversal`. `rev` searches a version-control revision instead of the current tree, and never combines with `pattern`, `traversal`, or `change`. `scope` reaches past the project tree: `global` answers collected package and runtime facts selected by the project's dependency context. Explicit `packages` replaces versions of named packages or adds packages the context lacks. `local@name` selects an accepted extra project; `local:name` selects a root-defined profile; `local@name:profile` selects a profile defined in that extra project's own `rift.toml`. `all` merges the primary project, profiles, accepted extra projects with their profiles, and global results. `target: "commit"` matches `query` alone against the messages of the commits the history store holds. Use `get_symbol` with an exact canonical identity returned by search. For a current-tree search, the published workspace is resolved exactly once and threaded through both the search index's revision check and the executed `ReadService::search` call: a concurrent rebuild between two separate resolutions could otherwise validate ranked units against one snapshot and merge them into results computed from another.
search takes a lexical query or a regex pattern, narrowed or extended by
a paths selector. paths.include and paths.exclude glob the project-relative path - the same
glob engine backs the workspace's [source] policy -
narrowing the indexed candidates before scoring.
paths.force_include reaches past both: a file [source] or .gitignore excluded from the
index is parsed on demand and searched alongside the indexed hits, up to 256 matched files per
request - the bound counts the files the request reaches outside the index, so a glob whose
matches the index already holds adds none. A request whose paths.force_include matches more
than that refuses with limit_exceeded rather than searching a silently truncated set; the
limit evidence names paths.force_include, the bound, and the match count.
The top-level include field is a separate selector: it requests extra payload per hit instead
of narrowing which files are searched. source attaches the excerpt around the hit; score
attaches the fused ranking value used to order the page, comparable only within this one answer.
This request narrows the search to the tests tree, then reaches one file the workspace's
[source] policy excludes from the index:
{
"query": "test_fit",
"paths": {
"include": ["tests/**"],
"force_include": ["tests/legacy/test_solver.py"]
}
}Full-text search
query matches declaration names, qualified names, signatures, and attached documentation,
and the contents of every indexed file, through a full-text index, and the identifiers it
carries against the indexed names. A multi-word query is answered first by the documents
carrying every term; only when that leaves the answer short do documents carrying some of
them join, behind every exact one. Double quotes keep a phrase together in both passes.
An identifier written inside a question reaches its declaration without being quoted:
where does encode_path get its escape set ranks encode_path through the identifier
matching, while the prose around it ranks through the full-text index.
The index ranks at most 1,000 units for one request. When more units match, the answer
carries a lexical_ranking_truncated warning naming that bound, and hits past it never reach
a page: paths selects among the ranked units, so only a narrower query brings the rest
inside the bound.
The read has its own bound of 1,000, applied to the candidates it ranks and again to the hits
it orders and pages: a read that reached it warns results_truncated.
A symbol whose only match sits in its documentation, not in its name, still returns as a
symbol hit. matched_by names every field that carried a term, so this one is tagged
["documentation"]. A hit tagged ["ranked"] is one the vector ranking placed, where no
indexed field carries the query's literal bytes at all.
Request:
{ "query": "loads workspace configuration", "target": "symbol" }{
"query": "loads workspace configuration",
"target": "symbol",
"include": [
"source"
],
"limit": 5
}The index holds a file's text once, in the file's own rows, and a declaration's row carries its names, signature, and documentation alone. A term only a declaration's body holds therefore ranks the file.
The server reads the first 20 file rows of the ranked list, finds the declarations holding the
query's terms in them, and answers those declarations in place of the first of those rows, best
first. Each is tagged matched_by: ["content"], and a declaration its own row placed ahead of
that file row keeps its place and its tags.
Every visible bounded UTF-8 file without a NUL byte joins the same index when its effective
language entry or [search.text].include selects it. A hit on one
of those files returns as a file hit, tagged matched_by: ["content"] the same way:
Request:
{ "query": "install checksum", "target": "file" }search result holding this hit, compact text:
1 result
README.md:35 · contentA path carrying no extension - justfile, Dockerfile - is selected by the same patterns and
returns the same way, because the selection reads the whole project-relative path. A file hit
carries languages only where a syntax provider claims the path; the full-text index also
reaches a file no provider claims, as this one does.
The hit above carries no source: a request pays that lookup only when include names it.
include: ["source"] attaches the excerpt as source on every hit; an omitted include returns
the hit's symbol or file, path, range, and line alone, as both hits above do.
A hit's score follows the same rule: absent unless include names it. include: ["score"]
attaches the fused ranking value that ordered the page, comparable only within this one
answer.
Pattern
pattern answers every match of a regular expression in the text of the indexed files, in place
of query. The syntax is the Rust regex crate's, the dialect ripgrep
reads, so a pattern written for rg or Claude Code's Grep carries over unchanged. Matching
runs line by line, as ripgrep's does:
^and$match at line boundaries- a
\rbefore a line feed belongs to its line - no class matches a line feed, so only a literal
\ncrosses a line (?i)makes the match case-insensitive; the request carries no case field
The server selects the files to read through a trigram index over the stored text, then verifies
each one with the pattern, so the answer equals a scan of every indexed file. A pattern whose
literals hold no run of three characters, such as [0-9a-f]{40}, verifies every file paths
selects.
The server fills the trigram index in the background after each write to the stored text, and no
search waits for it. Until the index catches up, the server verifies the rows it lacks beside the
rows it selects, while both fit the pattern_candidate_rows and pattern_verified_size bounds
below. Past either, the answer covers the rows the index holds and carries a
pattern_index_preparing warning.
With target: "file", every match is one hit: range holds the match's bytes, line its line,
and source, when include names it, the matched line. With target: "symbol", every
declaration holding a match is one hit at its first match, and range holds the declaration's
bytes; all answers both. Each hit is tagged matched_by: ["content"] and carries no score, so
relevance orders the hits by path, then offset, while path and identity order as they do in
every other search answer.
Request:
{
"pattern": "tokio::\\w+",
"paths": { "include": ["src/**"] },
"target": "file",
"include": ["source"]
}Response, compact text:
2 results
[1] src/main.rs:9 · content
#[tokio::main]
[2] src/main.rs:11 · content
let config = tokio::task::spawn_blocking(|| {A pattern search is bounded at every step it takes. The pattern holds at most 1,024
characters, the schema's maxLength, and the
[search] keys bound the rest:
pattern_compiled_size- the bytes the pattern's matcher compiles topattern_candidate_rows- the trigram index rows one search reads to select its filespattern_verified_size- the file text one search verifiespattern_matches_per_file- the matches one file contributes
A search past the length or one of the first three keys refuses invalid_request naming the
field pattern and the key, as in
more than 10000 candidate rows, the [search] pattern_candidate_rows bound; narrow `pattern` or `paths`.
A file past pattern_matches_per_file is cut there, and the answer carries a
pattern_matches_truncated warning naming it, while every other file answers in full.
results_truncated still reports the result bound alone.
The server refuses pattern beside query, traversal, change, or rev with
invalid_request: the trigram index holds the current tree alone, and the other fields select
another result set or tree. A scope past local reaches the package files through the global
index, as package scope describes. target: "documentation" refuses
capability_unavailable, since a documentation block carries no file text.
Traversal
traversal walks the references or calls a configured
language engine resolves: a bounded search
from one seed, following direction and narrowed to facets, up to depth hops. seed and
the optional to are declaration SymbolId
values, the same identity a get_symbol or search hit already carries.
traversal stands alone as a pure graph walk or rides beside query; a symbol both lanes
reach keeps its lexical score and gains the walk's traversal_path, and its matched_by
carries both entries.
direction names the edges the walk follows from each visited symbol:
incoming, the default - the engine answers which declarations reference the symbol, and each hop carries thereferencesfacet.outgoing- the engine's call hierarchy answers which declarations the symbol calls, and each hop carries thecallsfacet.
These shapes cover most questions:
- Impact -
seedalone,depthat its default: what would change if this symbol did. - Callees -
direction: "outgoing": what this symbol relies on, and atdepth: 2what those declarations rely on in turn. A callee in an installed package or a standard library is a hit carrying the global index's identity for it, and the walk stops there. - Path -
toset: whether, and by which shortest route, the walk reaches one named symbol.
Each reached symbol becomes a hit tagged matched_by: ["relationship"], carrying distance -
the path length - and traversal_path, the GraphHop list
from seed to the hit. Every GraphHop carries the
Relationship it followed and the direction it
followed it in; seed itself is never a hit.
The example project uses the canonical identity returned by search. Without a language engine that serves this seed, both directions refuse explicitly:
{
"traversal": {
"seed": "rift://symbol/local/rust/example_project/config/load_config",
"direction": "incoming",
"depth": 1
},
"target": "symbol",
"include": [
"source"
]
}{
"traversal": {
"seed": "rift://symbol/local/rust/example_project/config/load_config",
"direction": "outgoing",
"depth": 2
},
"target": "symbol",
"include": [
"source"
]
}With a configured engine, each caller's traversal_path carries a relationship from that caller
to the seed, with facets: ["references"] and derivation: "resolution", followed in the
incoming direction. An outgoing hop carries facets: ["calls"] and follows the relationship
from the seed to its callee. A callee in an installed package or runtime can carry the global
index's exact identity. Its physical unit names its defining source; a source-less builtin can
have no declaration binding. The walk stops at a global object.
facets narrows the walk to RelationshipFacet
values; an empty or omitted list follows every facet a lane populates, which is references on
an incoming walk and calls on an outgoing one. target: "file" never carries a walked hit,
since a traversal only reaches symbols.
A traversal names the declaration its walk starts at, so a request carrying no seed
refuses invalid_request naming the field.
A walk whose seed no engine answers for refuses capability_unavailable naming
relationship traversal (no language engine serves the seed): nothing this workspace holds
resolves a reference to that declaration, and a resend changes nothing. A facets list naming
no facet a lane populates draws the same refusal, such as references alone on an outgoing walk.
An engine session serves the current tree, so traversal combines with neither rev nor
change, and refuses capability_unavailable for each, whatever its direction.
An outgoing walk from a seed the ready engine prepares no call hierarchy item at refuses
capability_unavailable naming the seed's kind, such as a struct under rust-analyzer. The
engine answers that only once it reads ready, so a resend changes nothing.
A seed that resolves to no declaration refuses resource_not_found; one the engine answers
for with no callers or callees returns an empty result set, since an isolated declaration is a
legitimate answer.
A walk the server stops at its node bound attaches a traversal_truncated entry to
warnings, naming how many symbols it visited: hits reachable beyond the bound are missing,
and a tighter facets list, a smaller depth, or a less-connected seed fits the walk under it.
A walk asking for coverage no lane populates attaches a relationship_coverage_missing entry
to warnings. Its facets names the requested facets no lane populates, such as implements
beside the references the engine lane resolves. An empty answer carrying it means the walk
could not run; an empty answer without it means the walk ran and the seed has no neighbor
under the request.
An outgoing walk attaches a callees_dropped entry to warnings when the engine names callees
no declaration answers for: a file outside the project and every installed package, or a
position where the global index holds no declaration. With [global] enabled = false, or when
the global API does not answer, every callee in a package or a standard library drops, and the
answer also carries the global warning naming why, such as global_access_disabled. The walk
carries no edge to a dropped callee; callees counts the edges it dropped over every depth.
Language-engine references
A current-tree symbol traversal consults the configured
language engine when facets is empty or
names the engine facet of its direction: references for incoming, calls for outgoing.
The engine's answer is what the walk follows, under the same traversal depth and result bounds.
The server maps each reference location, and each callee's name in a project file, to the
smallest indexed declaration containing it. Locations outside the workspace or without an
indexed declaration contribute no hit. When no engine is configured, or the engine lacks
reference support, the walk refuses capability_unavailable. A configured engine that fails to
start returns its failure through ErrorData.
A callee outside the project maps to the package whose files hold it, at an exact version:
- an installed package the workspace's dependencies name, under its
node_modulesfolder, its Cargo registry folder, or its import folder in the Python environment, at the version the lockfile pins - the Rust standard library below
rustc --print sysroot, at the toolchain's version - a crate the Rust standard library vendors, such as
library/vendor/hashbrown-0.17.1/below the sysroot, as that Cargo package at the version the standard library'sCargo.lockpins - the TypeScript built-ins in the
lib.*.d.tsfiles of thetypescriptpackage the engine runs, at that package's version - Python's standard library, in the typeshed stubs the embedded engine ships, at the release the global API resolves for the workspace's Python
A package folder inside the workspace, such as node_modules or .venv, maps as its package.
The server then asks the global API which declaration holds each such callee's position, in one
request per walk, and the walk does not continue from a package callee.
Each dependency hit carries its own immutable view for subsequent get_symbol and
get_source reads. The caller MUST use that hit's view rather than the top-level search
view, which retains the project graph and paging selection. Each hit's view expires no later
than either the project or cloud selection. The proxy retains the originating project and
global endpoint for those reads and refuses a changed context instead of selecting another
publication.
The server captures source and configuration before and after the engine request. If either
changes, the server captures the current publication, refreshes the search ranking, and repeats
the reference request, with at most three attempts. The publication wait, the search ranking,
reference resolution, and these retries share one [server] readiness_timeout deadline, taken
once where the request enters. Exhausted attempts or an expired deadline return
temporarily_unavailable with retry: "same_request".
A stale publication uses indexed relationships and carries stale_index. Each engine request
also uses the configured engine retry and restart bounds.
A walk takes an engine's answer once the engine reads ready: the work it announced has ended and
stayed quiet for its settle_delay. The walk waits for that until a tenth of the
[server] readiness_timeout budget before the request's deadline, so the answer and its warning
still fit in the budget.
When that wait is spent, when the engine answers about bytes the served tree does not hold, or
when it answers more than the node bound holds, the walk drops the engine's contribution and
attaches an engine_analysis_unavailable entry to warnings, naming the seed's language. A
spent wait keeps the engine's session, so the engine goes on loading and a later read carries its
edges.
An engine that announced no work since it started, or since the server last sent it changed
files, gives no evidence that it settled. The walk takes its answer once the engine stayed quiet
for its settle_delay, and attaches an engine_readiness_unconfirmed entry naming the engine's
process key in processes.
Change
change answers the declarations two sides hold differently. base names the committed revision
compared from, spelled the way a revision read spells rev, such as main or
HEAD~1. head names the side
compared to: a revision spelled the same way, or the working tree, spelled
{"kind": "working_tree"}. An omitted head is HEAD.
The server lists the paths the two sides hold differently, reads and parses those paths alone on
each side, and matches declarations within one path by the provider's qualified name and kind. A declaration both revisions hold whose bytes are equal produces no hit; one whose
bytes differ is classified the way a symbol timeline classifies two adjacent
revisions, into signature_changed, body_changed, or decorators_changed. A declaration only
head holds is introduced, and one only base holds is removed.
A deletion and an addition in two different paths whose declarations match one to one - same
qualified name, same kind, same bytes - are one declaration that moved. Anything ambiguous
stays an addition beside a removal: two same-named declarations removed against one added leave
three hits, because no evidence says which removal the addition answers.
Each hit is tagged matched_by: ["change"] and carries a
SymbolChange as change, naming the kind
above and the base_path and head_path the declaration lives at on each side. The hit's
symbol, path, range, line, and source read the head revision:
This fixture changes the body of load_config from 42 to 43 and removes old_loader.
The comparison retains the changed declaration's head source and the removed declaration's
base source, each under its canonical identity:
{
"change": {
"base": "8422c73786ef5c78648ad3168949cb974e073e22",
"head": {
"kind": "working_tree"
}
},
"target": "symbol",
"include": [
"source"
],
"limit": 10
}A removed declaration is the exception: the head revision no longer holds it, so its hit keeps
its base-side identity, path, range, and source, and its change carries base_path alone.
Against the working tree, the head side is the current publication, so staged and unstaged edits,
files git does not track, and deleted files all take part, and the answer carries stale_index
while that publication lags the filesystem, as any current-tree read does. The base side reads
each changed file in the form the working tree holds it, so a checkout under
core.autocrlf=true compares CRLF bytes on both sides. A changed path whose attributes name a
filter driver, such as Git LFS, or a UTF-16 working-tree-encoding answers no declarations: the
server runs no driver during a comparison, and the answer carries a source_unavailable warning
naming the path and the driver or encoding.
Request:
{ "change": { "base": "HEAD", "head": { "kind": "working_tree" } }, "target": "symbol" }A comparison ranks nothing, so no hit carries a score even when include names one. Ordering
is deterministic for identical inputs: relevance has no score to rank by here and orders
changed declarations by path, then by name, while path and identity order as they do in
every other search answer. paths narrows which changed paths are compared, through the same
glob engine every other selector uses. target: "file" never carries a change hit, since the
comparison reaches declarations alone.
A comparison the server stops at its changed-path bound attaches a change_truncated entry to
warnings naming that bound: declarations in the changed paths past it are absent from the
answer, and paths narrows a further comparison onto them. The result set keeps its own bound
and its results_truncated warning. A changed file either revision cannot read - bytes past the
per-file byte bound, bytes that are not UTF-8, or a syntax tree the provider refuses -
contributes no declaration, and the changed paths beside it still answer.
The server refuses change beside rev with invalid_request, since change names its own
revisions, and beside query with invalid_request, since the two select different result
sets. A traversal beside it refuses capability_unavailable, since no lane serves the walk from
a comparison's changed declarations. A scope past local
refuses with invalid_request, because package facts are served for the current tree alone.
[providers.history] owns the capability the way it owns
revision-addressed reads: enabled = false refuses a comparison, a workspace
with no git repository refuses capability_unavailable, and a revision neither side resolves
refuses resource_not_found.
Commit search
target: "commit" matches query against the messages of the commits the
history store holds. The server parses the query the way a
full-text search does, then answers the commits whose message carries every
term before the ones carrying some, each group newest first by committer time, whatever order
names.
Under [providers.history] strategy = "selective" the store holds the selected releases alone, so
a commit search answers those.
While the store fills, a commit it has not analyzed yet answers nothing, and the answer carries a
history_store_filling entry in warnings: analyzed counts the commits the store holds of the
total its latest fill selects. The same warning appears when the store has not yet analyzed the
commit HEAD names, such as right after a new commit, and the read asks the fill to catch up:
search result carrying only this warning, compact text:
0 results
1 warning
history_store_filling · analyzed 40 · total 100: the history store holds 40 of the 100 commits its latest fill selects, so a commit it has not reached answers nothing; the fill runs in the background, and a later search answers the restEach hit carries a CommitHit as commit: the commit's
revision, its message, its author, the committer timestamp, and the paths it changed
against the commit it was compared with. The message is cut at 4,096 bytes on a character boundary,
and message_truncated says so; paths lists at most 32 paths in path order, and
paths_truncated says when the commit changed more. A commit hit carries no path, unit,
range, score, or matched_by:
Request:
{ "target": "commit", "query": "change path bound", "limit": 10 }search result holding this hit, compact text:
1 result
9c1d4e7a · Alice <alice@example.com> · 2026-09-22 14:03 +02:00
Bound comparisons at 512 changed paths
A comparison that reaches the bound answers from the paths that fit and warns change_truncated.
2 paths:
crates/rift-protocol/src/search.rs
crates/rift-server/src/change.rsThe server refuses target: "commit" beside pattern, traversal, change, rev, packages,
and paths, and with a scope past local, each with invalid_request naming the field: each of
them selects declarations, file text, or another tree, and the global index holds no project
history. A missing or empty query refuses invalid_request naming query.
[providers.history] enabled = false refuses
capability_unavailable, and so does a workspace whose server opened no history store, such as
one with no git repository.
Package scope
scope selects where query and pattern run. local, the default, selects the primary
project. local:name selects one profile defined in the root rift.toml. local@name
selects one extra project accepted by the connection, and local@name:profile selects one
profile defined in that extra project's own rift.toml. global selects collected package and
runtime facts named by the dependency context. Explicit packages replaces versions of named
packages or adds packages the context lacks. all merges the primary project, profiles,
accepted extra projects with their profiles, and global facts. Search returns each
logical owner in the canonical identity, preserving physical declaration locations separately:
{
"query": "beacon",
"scope": "all",
"target": "symbol",
"include": [
"source"
],
"limit": 10,
"paths": {
"include": [
"src/**"
]
}
}The caller MUST use query to combine profile and registered project results under all.
The server returns one combined page; each hit retains its captured view for exact reads.
Local search runs the indexed declarations and lines, paths.force_include, ranked tier,
and traversal. Global search runs the global query and pattern lanes. Under order: "path",
project paths precede released source units.
A package hit is addressed by unit in place of path - the
SourceUnitId of the package file - with range and
line inside that unit, and source when include names it. Its matched_by names the fields
the global index matched, such as name for its name and content for a term inside its body.
A package contributes no file to query, so target: "file" answers nothing from it.
A pattern reaches the package files too. The global index answers each match as a file hit
addressed by unit, with the file's size, and each declaration holding a match as a symbol
hit at its first match, both tagged matched_by: ["content"]. global answers released source
matches; all includes project matches too, retaining each matched owner.
The server refuses rev together with a scope beyond local as invalid_request, since
package facts are served for the current tree alone, and refuses traversal together with
global the same way, since a walk runs over the project alone; all still walks the
project graph. Package warnings under Reads ride a search answer whose
scope reaches packages. Exact reads report the selection and projection facts they retain.
The resolvers read at most 256 manifests per workspace, taken in path order, so a global or
all search over a workspace reaches packages those manifests declare. Exact get_symbol reads use the returned canonical identity.
The server asks the global API for one page per query phase, at most 1,000 declarations or the
smaller page the API advertises, and for one page of matches per pattern. A declaration page the
global API stopped at its response bound, or a pattern page it stopped at its limit, its file
bound, or its verified-text bound, answers what it holds and carries a global_page_warning with
warning_code: "result_truncated"; the server does not ask for the rest.
This search requests demo 1.1.0. The fixture serves collected release 1.0.0, reports
package_substituted, and returns that served owner in the symbol and source addresses:
{
"query": "beacon",
"scope": "global",
"packages": [
{
"manager": "cargo",
"registry": "crates.io",
"name": "demo",
"version": "1.1.0"
}
],
"target": "symbol",
"include": [
"source"
]
}packages names package versions this search reaches beside the snapshot context; see
requested packages.
Symbol retrieval
get_symbol
Reads one logical symbol by its canonical `id`. The caller may select a preceding immutable `view`; continuations retain that selection and its original expiry. A complete exact miss returns `symbol_not_found` and up to three alternatives. An unavailable selection returns its typed cause. Use `search` for discovery.
id selects one canonical logical symbol. Ownership and language are encoded in that
identity; declaration source addresses remain separate. get_symbol accepts neither
name discovery nor scope, packages, limit, or page_index.
{
"id": "rift://symbol/local/rust/example_project/config/load_config"
}outcome distinguishes found, missing, and unavailable. found contains one
symbol and its declaration bindings, including an empty declaration list for a source-less
object:
{
"id": "rift://symbol/stdlib/node@26.11.1/javascript/fs/readFile"
}{
"id": "rift://symbol/stdlib/cpython@3.12.9/python/builtins/len"
}missing means the exact selection was searched completely; symbol_not_found
names the requested identity and up to three actual structural alternatives. An incomplete
selection answers unavailable with a typed reason.
For missing, the server MUST set MCP isError to true and retain the same typed
answer in structuredContent under --output all. The --output text policy removes
structuredContent while retaining the error flag and compact text.
The search configuration bounds optional alternative
work through symbol_alternatives_work and symbol_alternatives_lowercase_work. Reached
bounds retain the definite miss and report incomplete alternative work.
{
"id": "rift://symbol/local/rust/example_project/config/load_configs"
}include selects projections: source adds declaration excerpts and node identities when
available, documentation adds bounded referring documentation, and history adds the
logical symbol's timeline. Omitted include defaults to ["source"]; [] requests none.
view retains a preceding immutable selection. Its expiry refuses the read; it never selects
latest implicitly. declaration_limit defaults to 5 and accepts 1 through 10000.
declaration_cursor continues the exact symbol, projections, and serving view. Each binding
retains origin, exactly one of path and unit, range, line, source_complete, and
indices into the symbol's signatures, types, and documentation.
{
"id": "rift://symbol/npm/npmjs.org/prettier@3.8.5/typescript/prettier/format"
}{
"id": "rift://symbol/npm/npmjs.org/prettier@3.8.5/typescript/prettier/format",
"declaration_limit": 1
}{
"id": "rift://symbol/npm/npmjs.org/prettier@3.8.5/typescript/prettier/format",
"declaration_limit": 1,
"declaration_cursor": "fixture-next",
"view": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}history follows the revisions held by the history store
from the served revision. It records introduced, body_changed, signature_changed,
moved, removed, and decorators_changed. A revision whose source cannot be parsed,
or where the symbol is absent, contributes no version. Commit author name and email remain
the commit's author; timestamps remain committer time. An unchanged file rename or unchanged
declaration move records moved and follows the previous path.
complete: false means history depth, a shallow clone, the oldest selected release, or a store
still filling ended the timeline before the repository's first commit. When the store did not
open, the server walks Git per request instead.
Symbol ownership
A local:name owner in a symbol identity selects a profile defined in the root configuration.
For example, rift://symbol/local:docs/typescript/page/beacon selects the docs profile.
A local@name:profile owner selects a profile defined in an extra project's own configuration,
such as rift://symbol/local@cloud:docs/typescript/page/beacon. Scopes and symbol URIs spell the
owner identically.
A local@name owner selects an extra project accepted by the connection:
{
"id": "rift://symbol/local@sdk/rust/docs_sdk/load_config"
}A package or runtime identity selects its exact served version. Search's scope and
packages discover these identities. Requested package substitutions are reported by search;
the returned identity names the served owner. Exact reads never silently replace that version.
The declaration's unit keeps the physical defining owner even when the logical owner differs.
{
"query": "beacon",
"scope": "global",
"packages": [
{
"manager": "cargo",
"registry": "crates.io",
"name": "demo",
"version": "1.0.0"
}
],
"target": "symbol",
"include": [
"source"
]
}{
"id": "rift://symbol/cargo/crates.io/demo@1.0.0/rust/demo/beacon",
"include": [
"source",
"documentation"
]
}{
"query": "beacon",
"scope": "global",
"packages": [
{
"manager": "cargo",
"registry": "crates.io",
"name": "demo",
"version": "1.1.0"
}
],
"target": "symbol",
"include": [
"source"
]
}rev selects a local Git revision. It is refused beside view and for package or runtime
identities. Unknown named local projects, expired views, and incomplete coverage are explicit
selection failures. A malformed identity is refused before lookup:
{
"id": "rift://symbol/rust/src/lib.rs/load_config"
}Source retrieval
get_source
Reads a canonical source unit or a half-open UTF-8 byte range. A preceding `view` retains its immutable selection and original expiry; `rev` selects a local Git revision. The result returns source bytes, definite absence or typed selection unavailability.
unit selects one physical source unit. Omitted range requests the whole unit; a supplied
range is half-open UTF-8 byte offsets. Documentation does not require a symbol declaration:
{
"unit": "rift://source/project/README.md"
}{
"unit": "rift://source/project/README.md",
"range": {
"start": 360,
"end": 398
}
}The view returned by the preceding named-project symbol read selects that project's
documentation. The project unit keeps its repository-relative path:
{
"unit": "rift://source/project/README.md",
"view": "e1c7db5b8bb6eca9c67b20e69cfa0bba919e60b0c522429052ea2147ccbaeebd"
}{
"unit": "rift://source/project/README.md",
"view": "e1c7db5b8bb6eca9c67b20e69cfa0bba919e60b0c522429052ea2147ccbaeebd",
"range": {
"start": 43,
"end": 81
}
}A released source read uses its physical owner address:
{
"unit": "rift://source/cargo/crates.io/demo@1.0.0/src/lib.rs"
}found returns source.span, source.text, source_complete, and an immutable view.
missing requires complete captured coverage; unavailable states why presence or absence
could not be established. A package or runtime source address retains its defining owner.
view retains a preceding selection; rev is refused beside view and for released sources.
Nodes
nodes
Lists the syntax nodes covering one UTF-8 byte position in one file, outermost first. Each identity carries a witness, so an address taken from this listing refuses cleanly once the file's bytes drift. `rev` lists the nodes as of a version-control revision instead of the current tree. If discovery is incomplete or a targeted read spends the request's remaining time, the result has empty `nodes` and `source` and carries `local_index_preparing`. A selected source path can answer within that remaining time, with the same warning until the publication records the path: workspace byte and declaration bounds remain unchecked. Per-file refusals keep their typed errors. A visible path no syntax provider parses refuses `capability_unavailable`, naming the extension.
A visible path no syntax provider parses - justfile, .gitignore - refuses
capability_unavailable, naming the extension, or stating that the path carries none.
During local preparation, the server MAY prepare the requested path within the remaining
readiness_timeout budget. An answer for a path outside
the published prepared files carries local_index_preparing, including when syntax
is returned; this read does not advance published progress. Exhausted preparation returns empty
nodes with that warning.
This call lists the nodes covering byte 338 of src/config.rs, inside the parse_config call:
{
"path": "src/config.rs",
"position": 338
}Reads
Callers read code through these tools:
| Tool | Result |
|---|---|
get_symbol | one logical symbol by canonical identity |
get_source | one physical source unit or byte range |
search | ranked files and symbols across the workspace tree |
nodes | the syntax nodes covering one position, with byte ranges |
Result content defines the representations a tool result carries and the proxy output selection that chooses them.
The index never follows or indexes a symlink, so no read returns one; the file a link points at answers under its own path.
search pages by page_index under limit, at most 10,000. Its pagination carries
the served page and true page count, including an empty page past the last page.
get_symbol bounds declaration bindings with declaration_limit and continues with
declaration_cursor. Search and exact reads return an immutable view with an expiry;
later reads can retain that selection. Expiry never selects latest implicitly.
Every read result also carries warnings. A stale_index warning reports an answer computed
from an index that lags the tree the read captured, and names the two tree revision digests that
disagree.
The server MUST carry local_index_preparing once while the requested local scope remains
incomplete. prepared counts visible files whose capture and analysis finished, including files
refused under an accepted bound; total is omitted until discovery finishes. ready_in is
omitted because the server has no preparation estimate.
An exact symbol read cannot establish complete declaration coverage while selected files remain unprepared. Global-only reads omit local preparation warnings. Preparation counts describe the answer's publication and can decrease when source selection changes.
A read served while the last rebuild's failure stands carries stale_index too, with a detail
naming the failure; the next filesystem event retries the rebuild. When its two digests are equal,
the tree moved in recorded files outside the syntax-indexed ones or in the configuration file, and
detail says which.
The search ranking tiers carry their own warnings:
vector_index_preparing- the vector ranking holds no vectors for the tree this answer was read from: the pass over that tree is still running, or the tree was published after the corpus the ranking holds was described. A query sharing no token with the code it describes reaches nothing until that pass lands;preparedandtotalstate how far it has got, and an answer carrying no such warning is one the vector ranking took part in.vector_ranking_unavailable- the vector ranking will not answer for the life of this server: the model weights could not be acquired, or the model failed to load.lexical_ranking_unavailable- the full-text tier has not yet committed the captured tree, missed a commit, holds no indexed tree, failed to load, got no database connection within[search] busy_timeout, or still answers for a newer publication after the request's capture attempts; thedetailnames which, with the tree revision in the first three cases. Ranking falls back to identifier matching, so a query phrased as prose finds nothing. The tier is read under the answer's own tree revision, so one that has moved past it is recaptured, up to those attempts, rather than warned about, a case a caller cannot otherwise tell apart from a tier that searched and found nothing. A pending commit keeps this warning in the answer; a later search can use the full-text tier after the commit lands.history_store_filling- a commit search answered while the history store lacks some of the commits its latest fill selects, or the commitHEADnames;analyzedandtotalstate how far the fill has got.lexical_ranking_truncated- the lexical ranking stopped atmatches_maxunits, and hits past that bound never reach a page, whateverpathsselects.total_pagescounts the units that did; a narrowerquerybrings the rest inside the bound.documentation_unavailable- the documentation this search projects onto crossed a bound, so no documentation block joins the answer and declarations and files answer as they would without it. Thedetailnames the documentation left out - the project's, theforce_includefiles', or the dependency packages' - and the bound it crossed.query_narrowed- the query carried more terms and quoted phrases thanterms_max, so the server dropped the shortest unquoted terms and ranked the answer by the rest. Every quoted phrase is kept. Shortenquery, or quote the terms that must be matched.results_truncated- the server reachedresults_max, its result bound, before ordering and paging. The bound covers both the candidates one read ranks and the hits it returns, so what it cut never reaches any page andtotal_pagescounts only what fit. The warning means the bound was reached: a read that cut nothing else but filled the candidate pool carries it too. A narrowerqueryorpathsbrings the read inside the bound.
A symbol_disagreement warning names a symbol whose selected presentation and at least one
provider's own facts disagree.
A source_unavailable warning names a file the index left out - bytes that are not valid UTF-8,
a file past its per-file bound, or a syntax tree the provider refused - so it answers no search or
lookup while every other file stays available. An answer carries at most eight of them, in
project-path order; when more files were left out, one more source_unavailable carries no unit
and counts the rest. rift server logs names each omitted file.
Text search answers can carry these warnings:
pattern_matches_truncated- a file held more matches ofpatternthan the[search]keypattern_matches_per_file, so its later matches are missing while every other file answers in full.filesnames the cut files, at most eight, in project-path order.pattern_index_preparing- the trigram index lacks rows of stored file text, and verifying them beside the selected rows would passpattern_candidate_rowsorpattern_verified_size, so a match in those rows is missing from the answer.preparedcounts the rows the index holds andtotalthe rows the store holds; the warning stops once the index has caught up.large_file_skipped- under[search.text] large_files = "skip", files pastmax_chunkare left out of the text index;skippedcounts the ones the request'spathsreach. A declaration such a file holds still answers.large_file_unparsed- files past[providers.syntax] max_fileare held as text the syntax provider does not parse: their text answerssearch, and none of their declarations answers a symbol hit orget_symbol.filesnames them, at most eight, in project-path order.lockfile_excluded- a lockfile the request'spaths.includeselects is left out of search under the[search.text]keyexcluded_lockfiles.rift://mapstill carries the versions it pins, and naming the file inpaths.force_includesearches it for one request.
A package-scope answer whose read never reached the global API, or met no answer from it, carries no package facts and one of these warnings naming why:
global_access_disabled-[global] enabledisfalse, so no package read reaches the global API.global_api_unavailable- the global API could not answer.failure_classidentifies the bounded failure.global_publication_incompatible- the global API advertised a publication format or corpus revision this client cannot read.failure_classnames which.global_response_invalid- the global API response is invalid or truncated.failure_classnames which.
Package-scope answers can also carry these package warnings:
global_page_warning- a valid global page carried a bounded condition.warning_codeidentifies it and optionaldetailexplains it.result_truncatednames a page the global API stopped early, and the answer holds what that page held.capability_unavailablenames a feature the global API's capabilities do not advertise, such aspatternsfor apatternsearch, and the answer then holds the project hits alone.package_absent- the global publication holds no release of the exactpackagethe dependency context names, so nothing answers for it.package_requirement_absent- the global publication holds no release the declared requirement inentryadmits, so nothing answers for it.package_substituted- the global publication holds no release at the versionentrynames, or none inside the range its requirement states, so the nearest collected release,package, answers in its place.package_unavailable- the dependency contextentrynames a package no public registry serves, so no global package index answers for it.reasonnames the capability Rift does not have yet, by the entry'savailability.package_context_degraded- a resolver read less than its manifests and lockfiles state, or a standard library probe answered nothing, so the dependency context can miss packages or name a standard library by its static reading.resolvernames which, by manager name -cargo,uv,npm, orbun- or by standard library entry -stdlib/rust,stdlib/node, orstdlib/python- andreasonsays what it could not read. When the context passes the entries one read sends - 20,000, or fewer when the global API advertises a smallerdependency_entries_max- the entries sorted last leave, andresolvernames their package manager, such aspypi. The request'spackagesleave only after every other entry has, the ones sorted last first, andreasonthen counts the requested packages not reported.
Each availability a package_unavailable warning names carries its own reason:
availability | Source | reason |
|---|---|---|
path | a path outside the workspace, or an archive at a path | Currently rift doesn't support indexing dependencies by path. If you're interested in this capability, please upvote it at https://github.com/volarized/rift/issues/392. |
git | a git repository | Currently rift doesn't support indexing dependencies from git repositories. If you're interested in this capability, please upvote it at https://github.com/volarized/rift/issues/393. |
private_registry | a registry other than the public one its package manager reads | Currently rift doesn't support indexing dependencies from private registries. |
url | a URL no registry serves, such as a uv wheel URL or an npm tarball URL | Currently rift doesn't support indexing dependencies from URLs. |
A path dependency inside the workspace is project source: the local index covers it, and the dependency context names no entry for it.
An answer carries at most eight package_context_degraded, package_unavailable,
package_absent, package_requirement_absent, and package_substituted warnings together: the
degraded resolvers first, in resolver order, then the entries no public registry serves, then the
packages and requirements the global publication lacks, then the entries a nearest release
answers for. The global warning naming why the global API did not answer rides before them,
outside that bound. The source_unavailable bound above is separate, so an answer can carry
eight of each.
Every read carries an optional rev, and reads the workspace tree when it is absent.
Result content
A completed tool result carries content with one text block of compact text. When the proxy selection is all, the result also carries structuredContent with the canonical result, and the tool listing carries outputSchema. The text block and structuredContent are rendered from the same answer.
A caller selects the representations when it starts the proxy with rift mcp --output:
all- The proxy forwardscontentandstructuredContent, listsoutputSchema, and forwards every content of a resource read.text- the default. The proxy MUST omitstructuredContentfrom each completed tool result andoutputSchemafrom every tool on everytools/listpage. The proxy MUST also omit theapplication/jsoncontents of a completed resource read that holds another content, so the compact text of the resource remains.
The proxy MUST keep its selection for its lifetime. A tool list that changes between pages or requests would break the caller's cache of the advertised tools.
Direct HTTP access to rift server returns both tool representations, because only the proxy
applies the selection. The workspace resource carries both contents over direct HTTP; the map
carries one JSON content.
The compact text of search and nodes lists items. Exact symbol and source reads state their
selected object or outcome and serving view. The text lists what an agent reads to choose its next call and leaves the normal state of each field out, and the facts of one line are separated by ·. Under the all selection, structuredContent carries every field of the result, including each field the layout leaves implicit.
The text is a sequence of sections. A section is a title line at column 0, then its entries, each indented by at least one level. A title is N and a noun, with the noun in the singular when N is 1, so a line at column 0 is always a title.
The server MUST indent with tabs, one tab per level, and MUST NOT indent with spaces. A caller then counts the levels of a line by its leading tabs, and the whitespace of copied text never reads as an indent. Copied text keeps its own whitespace after the indent.
The server MUST NOT write a blank line between a title and its first entry, between two items, or between two sections. A blank line inside an item stays, and the server writes it empty. The tree of a relationship walk, described below, is the one exception: a blank line separates its parts.
The server MUST write the items section first: N results, or 1 result, for search, and N nodes, or 1 node, for nodes. The title is written also when the result holds no item.
For search, the server MUST append · page P/T to the title of the items section when total_pages is greater than 1 or the requested page_index lies past the last page, where P is page_index plus 1 and T is total_pages. The title is the only place the text carries pagination, so a caller requests the next page with a page_index equal to P.
When compact text carries warnings, the server MUST write the warnings section after the
items section: the title N warnings, or 1 warning, counting the displayed groups and
other distinct warning lines. A result without displayed warnings has no warnings section.
The line of a warning has these parts:
- the
codeof the warning - each payload field except
detail, as· name value, in the order the warning declares them; a list joins its items with,, a package identity isname@version (manager), and a package context entry isname@versionorname requirementfollowed by(manager), with the availability added after the manager unless it iscanonical; a value holding·or:is quoted :and thedetail, when the warning carries one, quoted when it holds·or:
The server MUST group framework_context_unresolved warnings with equal framework
and detail. Each group omits unit from its heading and lists sorted, distinct file
identities below files:; other equal warning lines appear once. The structured warnings
array retains its original entries.
For canonical get_symbol, a proxy suppresses repeated framework warning text when the
distinct details and files remain unchanged for the same originating project, symbol owner,
and framework on that MCP connection. Changed facts or an intervening answer without that
framework make its text visible again. The proxy retains at most 256 observations, evicts
the oldest when full, and starts empty on a new MCP connection.
When the result holds more than one item, the server MUST start each item with [n] at one level, counting from 1 in result order, and write every further line of the item at two levels. A result with one item carries no marker and writes every line at one level.
A nodes result numbers its nodes the same way.
The server writes the lines of a hit in the order below and omits a line whose fact is absent. A location is path:line, or unit:line for a hit in a package source unit.
A symbol hit of search has these lines:
- the declaration: the display of the first signature, behind its visibility when the display does not start with it
- the location and the
matched_byfield names joined by,, followed by the exceptional facts below, all separated by· - the symbol identity, or the byte range as
START..ENDwhen the symbol has no identity - the first non-empty line of the first documentation text
- the change line, when the hit comes from a comparison
- a blank line, then the source
The change line is the kind alone, with its underscores written as spaces. When the change has a base_path and a head_path that differ, the line adds · and the two paths joined by →. When it has only one of them and that path differs from the location, the line adds that path.
A symbol hit of search whose traversal_path holds at least one hop is a walk hit. The server MUST NOT write walk hits as items. It writes them as a tree: the root is the symbol the walk starts from, and each hop of a traversal_path is one branch under the symbol the hop starts from.
The server MUST write the hits outside the walk first, as items, and the marker rule counts only those hits. The server MUST then write one tree per root, in the order the first walk hit of each tree has in the result.
The server builds a tree from the traversal_path of each walk hit:
- the root is the symbol the first hop starts from: the
fromof its relationship when thedirectionof the hop isoutgoing, thetowhen it isincoming - each hop adds one level, and its branch is the symbol the hop reaches: the
toof anoutgoinghop, thefromof anincominghop - paths that share their first hops share those branches, and the branches under one symbol keep the order their hits have in the result
- the hit sits on the branch of its last hop
The root writes every line at one level. A branch d levels under the root writes its head line at level 1 + d and every further line at level 2 + d.
The server MUST write a blank line before the root and before every branch, except before a root that is the first entry of the section. The further lines of a branch and the head line of a branch under it share a level, so the blank line marks where one ends.
The head line of a branch is ↳ and the relationship of its hop. The relationship is the facets of the hop's relationship, joined by , :
- an
outgoinghop writes each facet as its wire name with underscores as spaces, such ascallsordepends on - an
incominghop writes each facet read from the other end, as in the table below - a relationship whose
derivationisheuristicwritesprobablybefore its first facet
| Facet | incoming hop | Facet | incoming hop |
|---|---|---|---|
contains | contained by | aliases | aliased by |
declares | declared by | generates | generated by |
augments | augmented by | depends_on | dependency of |
references | referenced by | annotated_by | annotates |
calls | called by | throws | thrown by |
constructs | constructed by | catches | caught by |
reads | read by | bounded_by | bounds |
writes | written by | instantiates | instantiated by |
imports | imported by | specializes | specialized by |
exports | exported by | overloads | overloaded by |
extends | extended by | mixes_in | mixed in by |
implements | implemented by | embeds | embedded by |
has_type | type of | tests | tested by |
overrides | overridden by | configures | configured by |
binds | bound by |
A root whose symbol is a hit of the result writes the lines of that symbol hit at level 1, with the declaration as its first line and no word before it. Its location line starts with in , unless the hit has no location, and its symbol identity line starts with at . The further lines follow unchanged. Its location line leaves relationship out of the matched_by names, because the tree states it.
A branch whose symbol is a hit of the result writes its head line, then the lines of that symbol hit, all at level 2 + d, with these words before some of them:
bybefore the declaration, when every facet phrase of the hop ends inby, such asreferenced byorcalled by. The head line then leavesbyout of each phrase, as in↳ referenced, called, so thatbyheads the declaration line. When a phrase does not end inby, such astype of, the head line keeps the whole phrases and the declaration line has no word before it.inbefore the location line, which starts with the location. A hit without a location writes that line withoutin.atbefore the symbol identity line. A symbol with no identity writes its byte range withoutat.
The further lines of the symbol hit follow unchanged: the first documentation line, the change line, a blank line, and the source. The branch writes the location line without relationship in the matched_by names, as the root does.
A root whose symbol is no hit of the result writes one line: at and the symbol identity. A branch whose symbol is no hit writes its head line and the at line, and no other line. A branch is no hit when its own hit lies on another page. The root is no hit because a walk does not return the symbol it starts from. When a hit outside the walk is the root symbol, as a query beside the traversal can return it, the server MUST write that hit as the root and not as an item.
A file hit has the location and the matched_by field names on its first line, then the source directly below. A node hit adds the node identity between those two lines. A documentation hit starts with the heading path joined by >, or documentation without headings, then the location of the block and the matched_by field names, the identity of the symbol the block documents, and the source.
A commit hit has these lines:
- the revision, the author as
name <email>, and the date, hour, minute, and offset of the timestamp as2026-09-22 14:03 +02:00, separated by·; a requestedscoreends the line - the first line of the message
- a blank line, then the rest of the message
message truncated, whenmessage_truncatedistrue- a blank line, then
N paths:and one changed path per line, one level belowN paths:; whenpaths_truncatedistrue, the label isN+ paths · truncatedand a last line of...follows
A found get_symbol result writes the declaration, exact identity, exceptional facts,
then documentation and requested documentation context or history. A declarations: section
retains each physical location, byte range, available node identity, and requested source.
A returned declaration_cursor names remaining bindings. There is no hit count or hit paging.
A missing result writes error: symbol_not_found, the requested identity and bounded
alternatives below closest matches: when alternatives exist;
an unavailable result writes its typed reason and requested identity. A captured selection ends
with view ID · expires TIMESTAMP, followed by warnings when present.
A nodes result lists the nodes from the outermost to the innermost. Each node has these lines:
- the
kind, then· N lineswhen its excerpt holds more than one line, thengeneratedandtestwhen the node has those facets - the node identity
- the symbol identity, when the node has one
- the regions as
role START..END, joined by·, when the node has regions
After the last node, the server writes a blank line and the excerpt of the innermost node, indented like the further lines of an item, last in the items section. Each outer excerpt contains the inner ones, so structuredContent carries the excerpt of every node in source.
The server MUST NOT write a fact in its normal state. The absence of a fact therefore states the normal state, such as a project declaration that is authored, current, not deprecated, and not a test. The exceptional facts are:
scoreand its value, when the request asked forscore- the origin as its location and
name@version, when the declaration is not a project declaration without a package; the location isdependency,stdlib, orexternal generatedorsynthetic, whensource_kindis notauthoreddeprecatedandtest, when the symbol has those facetsdocument local, whendocument_localistrue- a truncation or an incomplete history, in the lines above
The layout leaves these fields to structuredContent:
- the language, container, modifiers, types, and extensions of a symbol, every signature beyond the first, and every documentation line beyond the first of a
searchhit - the byte range of a symbol that has an identity
- the
distanceof a walk hit, which the level of its branch shows, and therelationshipentry of itsmatched_by, which the tree shows - the size and languages of a file hit
- the unit, language, other facets, parent, and byte range of a node
- the
nodeof aget_symbolhit, thereferenceof a referenced block, thesourceanddocumentation_revisionof a documentation hit and of a referenced block, and thedocumentation_revisionof a documentation context
The server MUST write a value that is lowercase hex of at least 8 characters as its first 8 characters: the revision of a commit hit or a history entry, and each digest in a warning line or a resource text. The server resolves a request revision with git revision parsing, which accepts a commit id prefix, so a caller MAY send the cut value wherever a request takes a revision. structuredContent carries the full revision.
The server MUST write source, documentation text, and excerpts line by line,
with their bytes unchanged apart from the indent. A caller then recovers the text by
removing the leading tabs of the indent, with no unescaping. A documentation text that holds
only whitespace writes nothing. The subject of a commit message is one fact; the lines after
the blank line that follows the subject are written line by line, without the blank lines at
their start and the whitespace at the end of the message. The server writes every other fact on one line, without the spaces at its end, with
\n, \r, \t, and \u{HEX} for the control characters it holds. A value among the facts of a line that holds · or : is written between double quotes, with each " and \ inside written after a \; a value without either stays bare, and so does a value written alone on its line, such as a declaration, an identity, or a commit subject.
A caller that requests score, source,
documentation, or history finds it in the compact text. The server enforces a byte limit on one
answer's text and refuses an answer that crosses it.
The tabs below show the compact text of search, get_symbol, and nodes results, and of the warnings of an answer:
These examples retain compact text and canonical JSON from the same answers:
{
"query": "load configuration",
"include": [
"source"
],
"limit": 5
}{
"id": "rift://symbol/local/rust/example_project/config/load_config"
}{
"path": "src/config.rs",
"position": 338
}{
"query": "beacon",
"scope": "global",
"packages": [
{
"manager": "cargo",
"registry": "crates.io",
"name": "demo",
"version": "1.1.0"
}
],
"target": "symbol",
"include": [
"source"
]
}{
"traversal": {
"seed": "rift://symbol/local/rust/example_project/config/load_config",
"direction": "incoming",
"depth": 1
},
"target": "symbol",
"include": [
"source"
]
}Revision-addressed reads
Every read also accepts rev - a branch, tag, or commit id, spelled the way git spells it:
main, v0.0.6, dd0a482. The server resolves the revision in place from the workspace's
git objects, with no checkout, and indexes that commit's tree under the same
[source] policy and bounds as the working-tree scan. A revision
read answers from committed bytes, so uncommitted working-tree changes never appear in it.
This get_symbol request reads a declaration as it stands on main, whatever the working tree
currently holds:
{ "id": "rift://symbol/local/rust/example_project/config/load_config", "rev": "main" }A name may carry git's ancestry suffixes: ~N walks N first parents and ^N picks the Nth
parent, each count defaulting to 1, so HEAD~1 and HEAD^ name the commit below HEAD. The server
resolves the spelling through git's revision syntax, and a spelling that carries anything else -
a reflog selector such as HEAD@{1}, or a path after a suffix - refuses invalid_request naming
the field. This request reads the declaration one commit back:
Request:
{ "id": "rift://symbol/local/rust/example_project/config/load_config", "rev": "HEAD~1" }A revision search refuses force_include: reaching index-excluded files is a walk of the
working tree, which a revision does not have.
A workspace with no git repository refuses a revision read and names the remedy:
Execution failure, compact text:
1 error
capability_unavailable · retry operator_action
no configured provider serves this request: workspace /Users/alice/projects/charming-aurora, requires a git repository - run `git init`, or omit `rev` to read the current tree; adjust the request to a served capability, or configure a provider that serves itAn unknown revision refuses with resource_not_found, and
[providers.history] owns the capability: enabled = false
refuses every revision read.
Requested packages
packages names package versions a search reaches beside the ones the
workspace's manifests and lockfiles name, at most 64 entries. Each entry is a
RequestedPackage carrying manager, name, and an
optional exact version. The server applies the list to this read's dependency context alone:
- an entry naming a package the context holds replaces every entry for that package, a
pathorgitentry included - an entry naming a package the context lacks adds it
- an entry without
versiongoes out as the requirement>=0, which the global index answers from its newest collected release
Each entry then routes like any context entry. A requested version the global publication lacks
answers from the nearest collected release under package_substituted. A package with no
collected release answers package_absent, or package_requirement_absent for an entry without
version.
This search reads Tokio 1.47.1 beside the project, in place of the version the workspace's
Cargo.lock pins:
Request:
{
"query": "spawn_blocking",
"scope": "all",
"target": "symbol",
"limit": 10,
"packages": [{ "manager": "cargo", "name": "tokio", "version": "1.47.1" }]
}The server refuses packages as invalid_request naming the field beside the local scope, the
default, since a project read consults no package, and beside rev, since package facts are
served for the current tree alone. A list past 64 entries, and an entry whose manager, name,
or version is empty or longer than its schema allows, refuse the same way before anything
reaches the global API.
Resources
Resource contents
A read of rift://map answers contents with one JSON entry whose mimeType is
application/json. A read of rift://workspace answers with two entries, both naming the
requested uri: compact text with mimeType text/plain, then the JSON body with mimeType
application/json. The server makes both workspace contents from the same data.
The map's recommendations entries carry language, root, and command for installed
language servers without a selected configuration. The server MUST omit recommendations
when none remain and remove a matching entry from the next publication after accepting its
configuration. root is relative to the map's project, with . selecting the project itself.
A command is a rift config set invocation quoted for the server's operating system;
the caller runs it from the map project's root.
The listings of resources and resource templates declare application/json, the type of the JSON body. A caller SHOULD select a content by its mimeType, because the text output selection leaves compact text as the only content of a workspace read. The map remains JSON under both selections.
The compact text of the workspace resource follows the layout of a tool result: the facts of one line are separated by ·, a direction is written with →, a lowercase hex value of at least 8 characters is cut to its first 8, and the normal state of a fact is left out. Warnings form the section N warnings, or 1 warning, after the last section, and a section with no entry is omitted. The server enforces a byte limit on the text and refuses a read whose text crosses it as a protocol failure, with the typed ErrorData as data.
Workspace
rift://workspace reads accepted configuration together with one captured source catalog page.
The answer carries:
configuration_revision- an eight-character digest of the acceptedrift.tomlbyteslanguages- effective identities in order, with enabled state, include and exclude patterns, syntax availability, and selected LSP process statesource- captured project paths, content digests, and matched language identitiespagination- the zero-based page and total page countwarnings- conditions that qualify the page, omitted when empty
The bare URI selects page zero. rift://workspace?page_index=1 selects another page. Each page
holds at most 1000 source units; language summaries repeat so every page can be read
alone. A page past the end returns an empty source list with the true page count.
A read answers the two contents of a resource. The compact text has these parts:
- the header
workspace <configuration_revision>, with· page P/Tadded under the rule of the tool results - directly below the header, the sections
languagesandsource, in that order, each a title such aslanguages:with its entries indented by one level; the server MUST omit a section that has no entry - a language entry: the language identity,
disabledwhenenabledisfalse,syntaxandexecutionwhen they hold, andlsp <process> <state>when the language has a selected LSP process - under a language entry,
includeandexcludelines with their patterns joined by,, each only when the list is not empty - a source entry as
path · language · digest, without the language when none matched - after the last section, when the page carries warnings, the section
N warnings, or1 warning, with one line per warning as in a tool result
The tabs below show the page of a workspace with one TypeScript language and one source unit:
Request: rift://workspace
Response, compact text:
workspace 3f9a1c2e
languages:
typescript:tsx · syntax · lsp typescript:tsx ready
include src/**/*.tsx
exclude src/generated/**
source:
src/view.tsx · typescript:tsx · 8a4d20bcThe source page describes one published catalog. The server MUST return its configuration
revision, source digests, pagination, and preparation warning from that publication. During
preparation, the page carries local_index_preparing and lists prepared files;
after preparation, the catalog also includes visible files without enabled syntax.
Errors
A failure travels by its class. A tool execution failure is a Rift operating failure on a
get_symbol, search, or nodes call, such as invalid input, a crossed limit, or a storage
failure. A protocol failure is any other failure: an unknown method or tool, a malformed
request envelope, a transport failure, a resource read failure, or an MCP error forwarded from
upstream.
Tool execution failures
The server MUST answer a registered tool execution failure with a completed tool result that has
isError set to true and one content text block of compact text. The result MUST NOT
carry structuredContent under either output selection. The MCP specification
reports tool execution errors
in the result, where the model can read the failure and adjust its request.
The advertised outputSchema describes typed tool answers, including the get_symbol
missing outcome; registered operating failures carry no such answer. A proxy started
with --output text forwards the failure unchanged: it keeps isError and content, and the
result had no structuredContent to remove.
The text of a failure is one section. Its title is N errors, or 1 error, where N is 1 plus the number of entries of causes. Its entries are the failure, then each entry of causes, outermost first, as one list. Each line is written on one line, with control characters as escapes:
- the head of an entry, at one level:
<code> · retry <directive>, thecodeandretryofErrorDataor of the cause - below the head, at two levels, the
messageof the entry; an empty message writes no line - below the failure's message,
limit <field>: <required> over <limit>, when the failure carrieslimit - then one line for each entry of
diagnostics, as theseverity, the code in brackets, the file of the span followed by:line:column, and: <message>, each part only when the diagnostic carries it - after the diagnostics, the registered identity on its own line, at two levels, for a registered failure
Every entry writes its code and retry, also a cause whose code or retry equals the failure's.
The text omits phase. The typed ErrorData model carries it, and a protocol failure carries it in the JSON-RPC data as described under protocol failures.
The tabs below show execution failures as compact text, and the completed tool result of the first:
Execution failure, compact text of a request whose limit is below 1:
1 error
invalid_request · retry never
the request does not match the documented form: field limit, violation zero; correct the reported field and resend the request
rift.server.read_invalidEvery failure carries a retry directive, which is the field to branch on - it rides each error
instance rather than the code, because the same storage_failure can be transient once and
permanent the next time:
never- the same bytes will fail the same waysame_request- it was transient: an index still filling, a tree with a filesystem write in flightoperator_action- the workspace requires a local state or configuration change before retry
The phase field of ErrorData names the operation that failed while serving the request; its only value, read, covers reading
workspace or provider data. A limit_exceeded also carries which limit and by how much, so a
request can be resized without parsing the message meant for a human. Any other code omits
limit.
Each entry of causes holds a code, a message, and a retry for one link of the failure
chain, outermost first. The retry of the failure governs the call; a cause's own retry
says what could be done about that cause. The text writes the code and the retry of every cause.
Protocol failures
A protocol failure stays a JSON-RPC error. An MCP error forwarded from upstream keeps its received code, message, and data, so the proxy adds nothing to a refusal the server classified.
A failure the proxy raises itself carries the same typed ErrorData as the JSON-RPC data
under code -32000: a tools/call whose arguments is not an object, a forwarded request that
the server did not answer within the proxy's budget, and a server that is still building or
failed to start. The caller SHOULD read retry from that data as it reads it from the
first entry of an execution failure. A resource read refusal, such as an unknown resource URI,
carries a code and a message and no data. A read whose compact text crosses the byte limit carries the typed ErrorData as data.