asgrep-mcp exposes hierarchical ast-sgrep code retrieval (separate keyword / AST / semantic channels; no auto-fusion) to AI agents via the Model Context Protocol over stdio.
Pick one agent surface per client:
- MCP hosts → this server (
asgrep-mcp) - Pi → Code Mode (
pi install npm:pi-ast-sgrep) — do not also enable this MCP server in that Pi session
They share ast-sgrep-core but must not be stacked. See codemode.md.
git clone https://github.com/AdityaVG13/ast-sgrep
cd ast-sgrep
cargo install --path crates/ast-sgrep-mcp
# or from the workspace root after `cargo build --release`:
# ./target/release/asgrep-mcpAdd the server to your client's MCP config:
{
"mcpServers": {
"ast-sgrep": {
"command": "asgrep-mcp",
"env": {
"ASGREP_ROOT": "/path/to/your/repo",
"ASGREP_LIMIT": "16"
}
}
}
}Environment variables:
| Variable | Purpose |
|---|---|
ASGREP_ROOT |
Project / workspace root (default: cwd). Tool root args must stay under this jail. |
ASGREP_INDEX_PATH |
Privileged sink — absolute writable DB path. Pins which file; rebuilds stay in-place (no generation swap). |
ASGREP_DURABILITY |
strict | balanced | fast-unsafe (MCP inherits; FastUnsafe is power-loss risky) |
ASGREP_LIMIT |
Max hits per search (default 16) |
ASGREP_NO_EMBED |
Set to 1 to disable semantic pass |
Three nonfused retrieval channels. Each accepts query, optional root, and optional limit, and returns abbreviated one-line previews plus stable file#Lstart-Lend node IDs. The agent chooses the granularity; MCP never auto-fuses channels.
keyword_search: indexed lexical evidence only.ast_search: AST pattern evidence only.semantic_search: embedding evidence only.
code_search remains a deprecated compatibility alias for keyword_search; it no longer auto-fuses channels.
Expands 1 to 20 selected node IDs into full code. Optional context_lines reads adjacent lines and max_chars sets an aggregate response budget. Reads enforce project containment, strict UTF-8, regular files, and scan bounds.
Index statistics: file/symbol/chunk counts, embed backend, IVF sidecar presence.
Build or incrementally update the index. Pass force: true for full reindex.
Concurrency and cancel (intentional limits for trusted local agents):
- stdio is official
rmcp(tokio). The reader stays live while search/index run on blocking threads, sopingandnotifications/cancelledare not stuck behindindex_repo. - Concurrent
index_repocalls still share a process-wide single-flight lock; wait time counts toward a soft wall deadline (600s). The deadline is checked before start and after index work finishes. If the post-mutation check fails, the error notes that the index may already have committed (caches are still invalidated). notifications/cancelledsets the indexer's cooperative cancel flag. The walk/prepare loop returnsoperation cancelledwithout a new commit when the flag is observed before the bulk write. A cancel that arrives after commit is ignored, matching the MCP spec.
index_repoon first open (or rely on priorasgrep index .).- Choose one of
keyword_search,ast_search, orsemantic_searchwith a bounded limit. - Inspect abbreviated previews and retain only relevant node IDs.
- Call
code_readfor selected IDs, adding adjacent context only when needed. - Use the one-shot CLI when automatic fusion is explicitly desired. Structural rewrites, multi-statement templates, and YAML rules are out of contract; use standalone ast-grep, they are not silently delegated (
DISC-pattern-native-subset). Single-statement nested templates (fn $N($$$) { $STMT },if ($COND) { $BODY }) are native — seedocs/structural-patterns.md.
| Surface | Best for |
|---|---|
MCP (asgrep-mcp) |
Headless agents, Cursor Cloud, Claude Desktop |
LSP (asgrep-lsp) |
In-editor defs/refs/call hierarchy |
Both use the same .asgrep/ index.
Compact mode is the lowest-token CLI search contract. It emits one minified JSON value, deduplicates paths, omits absent and decorative fields, preserves rank order, and applies hard snippet ceilings:
asgrep --json --format compact \
--snippet-tokens 96 --response-snippet-tokens 768 \
"hybrid ranking fusion" .The compact payload uses this versioned schema:
{"h":[["2jl...:10-42","d","t","refresh","fn refresh()"]],"p":{"2jl...":"src/auth.rs"},"q":"query","v":1,"zb":[96,768,12],"zn":1,"zt":0}-
pmaps stable base-36 path hashes to paths. Repeated paths occur once. -
A
pentry is either a plain path string or, when a shared directory prefix is worth folding,[root_index, suffix]into the optionalrroot table:{"r":["crates/ast-sgrep-core/src/"],"p":{"2jl...":[0,"search/mod.rs"]}}ris present only when folding is strictly smaller than the verbatim table, measured on serialized bytes, so the encoding can never inflate a result set with no shared structure. Both forms can appear in one table. Useast_sgrep_plugins::resolve_compact_pathsrather than decoding by hand. -
Each
hrow is[id, kind, signal, symbol, snippet]in rank order. -
idis<path-id>:<start>-<end>. Pass it straight to the MCPcode_readtool, which resolves path ids from the same session. Outside a session, expand it top[path-id]#L<start>-L<end>;code_readretains its canonical path and containment validation either way. -
Kind codes are
xexact,ddefinition,ccaller,ggraph,aanchor,iimport,ppattern, andeembedding. Signal codes arexexact,tstructural, andmsemantic. -
zbis[per-result ceiling, response ceiling, used],znis the hit count, andztcounts snippets cut by either ceiling. Metadata is never dropped when snippet budget is exhausted. -
A snippet of
~means this MCP session already sent that exact body for that id, so it was not sent again;zecounts how many were elided. Reuse the earlier result or callcode_read. Elision is keyed on a content hash, so an edited file re-sends in full, and it is cleared byindex_reposo it never spans index generations. Passresend_seen: trueif your client does not retain earlier results.zeappears on the MCP surface only. -
Per-call accounting is named
z*on purpose.serde_jsonorders object keys alphabetically, so this keeps content keys (h,p,q,v) in a stable head and confines volatile numbers to a trailing block a consumer can strip. Repeated identical searches are byte-stable.
Every search tool declares an outputSchema, and tools/call returns typed
structuredContent alongside the minified text fallback, so a current client
parses results directly instead of reverse-engineering the compact envelope.
The two always agree: the text is the same JSON, minified.
initialize negotiates. A client asking for 2024-11-05 keeps it, a client
asking for 2025-11-25 gets it, and an unrecognized revision is answered with
the server's current revision. The server deliberately does not advertise
2026-07-28: that revision replaced this handshake lifecycle with
server/discover, which this stdio server does not implement.
A search that finds nothing returns a diagnostic envelope instead of an empty hit list, because the four causes below need four different next moves:
{"h":[],"next":"drop the lang filter","q":"absent_symbol","scope":{"lang":"rust"},"tried":["lexical"],"v":1,"why":"filters_excluded_all","zn":0}whyis one ofempty_index,filters_excluded_all,channel_unavailable, orno_match. An empty index outranks the other explanations, and filters outrank a genuine absence.triedlists the channels that actually ran;downlists any that could not.scopeechoes the effective filters, so the agent can see what excluded its candidates.nextis exactly one actionable step, not a menu.
The miss envelope is far cheaper than the result envelope it replaces (131 vs 421 bytes against the agent format on a fixed query). The point is not only the bytes: an unexplained empty result drives speculative retries that cost more than the search did.
A token unit is one UTF-8 byte. This conservative, deterministic ceiling is model-independent and cannot underestimate byte-fallback tokenizers. Limits are bounded to 4,096 per result and 65,536 per response; zero is valid. The fixed-query identity and 89.0% reduction evidence is recorded in compact output validation.
For agent pipelines where context is the budget, capsule mode returns refs
and one-line previews instead of full excerpts -- roughly 3x smaller than
the agent format at the same limit, with identical ranking:
asgrep --json --format agent-capsule --limit 5 "hybrid ranking fusion" .Each hit carries file, symbol, kind, score, lines, a preview
(first non-empty line, <=120 chars), and a ref like
crates/ast-sgrep-core/src/search/mod.rs#L120-L132. Bodies appear only on request:
- re-run with
--excerpt-lines Nto inline up to N lines per hit, or - hand the
refspan to your own file reader.
Capsule hits are meant to stay cheap: resolve only the spans you need with
your own file reader (editor API, sed/nl, MCP filesystem tools, etc.):
# Example: search, then read only the top hit span
asgrep --json --format agent-capsule 'auth refresh' .
# Each hit has file + lines.start/end + ref; open that window in your editor
# or agent file-read tool -- no special host product required.This keeps the search step capsule-cheap and defers content bytes to the reader, which can apply its own caching and token budgets.
Portable skills + MCP wiring live in packages/agent-plugin (Agent Plugins 1.0). Clients that load Agent Plugins use that directory as the plugin root; mcp.json launches asgrep-mcp on stdio.