rift
DeveloperProtocol

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-character Digest of the indexed tree the map was computed from
  • languages - per-language file and symbol counts, sorted by language spelling
  • modules - the directory tree indexed files sit under, each entry's file and symbol counts inclusive of its descendants, folded three directories deep
  • hubs - the most-referenced symbols, ranked by reference count descending
  • entry_points - symbols where execution starts
  • docs - markdown files, in path order
  • module_relationships - the modules a resolved reference reaches between, ranked by that count descending
  • packages - the packages the workspace's manifests and lockfiles state, in manager, name, then selector order
  • pagination - always page zero of one, the whole map on one page
  • warnings - 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 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 · content

A 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 \r before a line feed belongs to its line
  • no class matches a line feed, so only a literal \n crosses 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 to
  • pattern_candidate_rows - the trigram index rows one search reads to select its files
  • pattern_verified_size - the file text one search verifies
  • pattern_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 the references facet.
  • outgoing - the engine's call hierarchy answers which declarations the symbol calls, and each hop carries the calls facet.

These shapes cover most questions:

  • Impact - seed alone, depth at its default: what would change if this symbol did.
  • Callees - direction: "outgoing": what this symbol relies on, and at depth: 2 what 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 - to set: 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_modules folder, 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's Cargo.lock pins
  • the TypeScript built-ins in the lib.*.d.ts files of the typescript package 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 rest

Each 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.rs

The 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.

Payloads
input
Request:
{
: "rift://symbol/local/rust/app/parser/parse",
: [
"source"
]
}
output
Response:
{
"id": "rift://symbol/local/rust/app/parser/parse",
"outcome": "unavailable",
"reason": "insufficient_coverage"
}

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.

Payloads
input
Request:
{
: "rift://source/project/src/lib.rs"
}
output
Response:
{
"outcome": "unavailable",
"reason": "insufficient_coverage",
"unit": "rift://source/project/src/lib.rs"
}

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.

Payloads
input
Request:
{
: "src/config.rs",
: 338
}
output
Response:
{
: [
{
: "rift://node/rust/src/config.rs@0-356#dcbef6dd",
: "source_file",
: "rust",
: {
: 356,
: 0
},
: "rift://file/src/config.rs"
},
{
: [
"declaration",
"definition"
],
: "rift://node/rust/src/config.rs@218-355#67ecfb36",
: "function_item",
: "rust",
: "rift://node/rust/src/config.rs@0-356#dcbef6dd",
: {
: 355,
: 218
},
: [
{
: {
: 236,
: 225
},
: "name"
},
{
: {
: 355,
: 281
},
: "body"
}
],
: "rift://symbol/local/rust/app/config/load_config",
: "rift://file/src/config.rs"
},
{
: "rift://node/rust/src/config.rs@281-355#4e554fa8",
: "block",
: "rust",
: "rift://node/rust/src/config.rs@218-355#67ecfb36",
: {
: 355,
: 281
},
: "rift://file/src/config.rs"
},
{
: [
"expression"
],
: "rift://node/rust/src/config.rs@334-353#4df4426e",
: "call_expression",
: "rust",
: "rift://node/rust/src/config.rs@281-355#4e554fa8",
: {
: 353,
: 334
},
: "rift://file/src/config.rs"
},
{
: "rift://node/rust/src/config.rs@334-346#03f22dac",
: "identifier",
: "rust",
: "rift://node/rust/src/config.rs@334-353#4df4426e",
: {
: 346,
: 334
},
: "rift://file/src/config.rs"
}
],
: [
"use std::path::Path;\n\nuse crate::error::ConfigError;\n\n/// Workspace configuration read from `rift.toml`.\npub struct Config {\n pub root: std::path::PathBuf,\n}\n\n/// Loads the workspace configuration from `rift.toml`.\npub fn load_config(path: &Path) -> Result<Config, ConfigError> {\n let text = std::fs::read_to_string(path)?;\n parse_config(&text)\n}\n",
"pub fn load_config(path: &Path) -> Result<Config, ConfigError> {\n let text = std::fs::read_to_string(path)?;\n parse_config(&text)\n}",
"{\n let text = std::fs::read_to_string(path)?;\n parse_config(&text)\n}",
"parse_config(&text)",
"parse_config"
]
}

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:

ToolResult
get_symbolone logical symbol by canonical identity
get_sourceone physical source unit or byte range
searchranked files and symbols across the workspace tree
nodesthe 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; prepared and total state 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; the detail names 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 commit HEAD names; analyzed and total state how far the fill has got.
  • lexical_ranking_truncated - the lexical ranking stopped at matches_max units, and hits past that bound never reach a page, whatever paths selects. total_pages counts the units that did; a narrower query brings 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. The detail names the documentation left out - the project's, the force_include files', or the dependency packages' - and the bound it crossed.
  • query_narrowed - the query carried more terms and quoted phrases than terms_max, so the server dropped the shortest unquoted terms and ranked the answer by the rest. Every quoted phrase is kept. Shorten query, or quote the terms that must be matched.
  • results_truncated - the server reached results_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 and total_pages counts 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 narrower query or paths brings 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 of pattern than the [search] key pattern_matches_per_file, so its later matches are missing while every other file answers in full. files names 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 pass pattern_candidate_rows or pattern_verified_size, so a match in those rows is missing from the answer. prepared counts the rows the index holds and total the rows the store holds; the warning stops once the index has caught up.
  • large_file_skipped - under [search.text] large_files = "skip", files past max_chunk are left out of the text index; skipped counts the ones the request's paths reach. A declaration such a file holds still answers.
  • large_file_unparsed - files past [providers.syntax] max_file are held as text the syntax provider does not parse: their text answers search, and none of their declarations answers a symbol hit or get_symbol. files names them, at most eight, in project-path order.
  • lockfile_excluded - a lockfile the request's paths.include selects is left out of search under the [search.text] key excluded_lockfiles. rift://map still carries the versions it pins, and naming the file in paths.force_include searches 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] enabled is false, so no package read reaches the global API.
  • global_api_unavailable - the global API could not answer. failure_class identifies the bounded failure.
  • global_publication_incompatible - the global API advertised a publication format or corpus revision this client cannot read. failure_class names which.
  • global_response_invalid - the global API response is invalid or truncated. failure_class names which.

Package-scope answers can also carry these package warnings:

  • global_page_warning - a valid global page carried a bounded condition. warning_code identifies it and optional detail explains it. result_truncated names a page the global API stopped early, and the answer holds what that page held. capability_unavailable names a feature the global API's capabilities do not advertise, such as patterns for a pattern search, and the answer then holds the project hits alone.
  • package_absent - the global publication holds no release of the exact package the dependency context names, so nothing answers for it.
  • package_requirement_absent - the global publication holds no release the declared requirement in entry admits, so nothing answers for it.
  • package_substituted - the global publication holds no release at the version entry names, or none inside the range its requirement states, so the nearest collected release, package, answers in its place.
  • package_unavailable - the dependency context entry names a package no public registry serves, so no global package index answers for it. reason names the capability Rift does not have yet, by the entry's availability.
  • 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. resolver names which, by manager name - cargo, uv, npm, or bun - or by standard library entry - stdlib/rust, stdlib/node, or stdlib/python - and reason says what it could not read. When the context passes the entries one read sends - 20,000, or fewer when the global API advertises a smaller dependency_entries_max - the entries sorted last leave, and resolver names their package manager, such as pypi. The request's packages leave only after every other entry has, the ones sorted last first, and reason then counts the requested packages not reported.

Each availability a package_unavailable warning names carries its own reason:

availabilitySourcereason
patha path outside the workspace, or an archive at a pathCurrently 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.
gita git repositoryCurrently 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_registrya registry other than the public one its package manager readsCurrently rift doesn't support indexing dependencies from private registries.
urla URL no registry serves, such as a uv wheel URL or an npm tarball URLCurrently 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 forwards content and structuredContent, lists outputSchema, and forwards every content of a resource read.
  • text - the default. The proxy MUST omit structuredContent from each completed tool result and outputSchema from every tool on every tools/list page. The proxy MUST also omit the application/json contents 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 code of 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 is name@version (manager), and a package context entry is name@version or name requirement followed by (manager), with the availability added after the manager unless it is canonical; a value holding · or : is quoted
  • : and the detail, 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_by field names joined by , , followed by the exceptional facts below, all separated by ·
  • the symbol identity, or the byte range as START..END when 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 from of its relationship when the direction of the hop is outgoing, the to when it is incoming
  • each hop adds one level, and its branch is the symbol the hop reaches: the to of an outgoing hop, the from of an incoming hop
  • 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 outgoing hop writes each facet as its wire name with underscores as spaces, such as calls or depends on
  • an incoming hop writes each facet read from the other end, as in the table below
  • a relationship whose derivation is heuristic writes probably before its first facet
Facetincoming hopFacetincoming hop
containscontained byaliasesaliased by
declaresdeclared bygeneratesgenerated by
augmentsaugmented bydepends_ondependency of
referencesreferenced byannotated_byannotates
callscalled bythrowsthrown by
constructsconstructed bycatchescaught by
readsread bybounded_bybounds
writeswritten byinstantiatesinstantiated by
importsimported byspecializesspecialized by
exportsexported byoverloadsoverloaded by
extendsextended bymixes_inmixed in by
implementsimplemented byembedsembedded by
has_typetype ofteststested by
overridesoverridden byconfiguresconfigured by
bindsbound 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:

  • by before the declaration, when every facet phrase of the hop ends in by, such as referenced by or called by. The head line then leaves by out of each phrase, as in ↳ referenced, called, so that by heads the declaration line. When a phrase does not end in by, such as type of, the head line keeps the whole phrases and the declaration line has no word before it.
  • in before the location line, which starts with the location. A hit without a location writes that line without in .
  • at before the symbol identity line. A symbol with no identity writes its byte range without at .

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 as 2026-09-22 14:03 +02:00, separated by ·; a requested score ends the line
  • the first line of the message
  • a blank line, then the rest of the message
  • message truncated, when message_truncated is true
  • a blank line, then N paths: and one changed path per line, one level below N paths:; when paths_truncated is true, the label is N+ paths · truncated and 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 lines when its excerpt holds more than one line, then generated and test when 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:

  • score and its value, when the request asked for score
  • the origin as its location and name@version, when the declaration is not a project declaration without a package; the location is dependency, stdlib, or external
  • generated or synthetic, when source_kind is not authored
  • deprecated and test, when the symbol has those facets
  • document local, when document_local is true
  • 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 search hit
  • the byte range of a symbol that has an identity
  • the distance of a walk hit, which the level of its branch shows, and the relationship entry of its matched_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 node of a get_symbol hit, the reference of a referenced block, the source and documentation_revision of a documentation hit and of a referenced block, and the documentation_revision of 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 it

An 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 path or git entry included
  • an entry naming a package the context lacks adds it
  • an entry without version goes 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 accepted rift.toml bytes
  • languages - effective identities in order, with enabled state, include and exclude patterns, syntax availability, and selected LSP process state
  • source - captured project paths, content digests, and matched language identities
  • pagination - the zero-based page and total page count
  • warnings - 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/T added under the rule of the tool results
  • directly below the header, the sections languages and source, in that order, each a title such as languages: with its entries indented by one level; the server MUST omit a section that has no entry
  • a language entry: the language identity, disabled when enabled is false, syntax and execution when they hold, and lsp <process> <state> when the language has a selected LSP process
  • under a language entry, include and exclude lines 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, or 1 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 · 8a4d20bc

The 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>, the code and retry of ErrorData or of the cause
  • below the head, at two levels, the message of the entry; an empty message writes no line
  • below the failure's message, limit <field>: <required> over <limit>, when the failure carries limit
  • then one line for each entry of diagnostics, as the severity, 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_invalid

Every 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 way
  • same_request - it was transient: an index still filling, a tree with a filesystem write in flight
  • operator_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.

On this page