PASR
Provenance-Aware Span Recall — a zero-setup context broker your coding agent calls
as an MCP tool. It hands the model the few lines that answer the question, each with a
file:line and the reason it was picked — and tells the agent when it asked the wrong
tool.
The problem
An agent working in a real repo has two bad options: read whole files and burn its context window on code that never mattered, or grep-and-guess and miss the file that held the answer. Either way, you cannot see what it looked at — no record, no line numbers, no reason.
What PASR does
Budgeted
You set a token ceiling; PASR never returns more, and it packs whole spans — never a truncated function. If the full file set already fits, you get it back unchanged.
Traceable
Every span carries file:line, a token count, its retrieval score, and why it was
kept (bm25, symbol, active_window, …). A byte-stable receipt — of what was kept
and what was dropped — lands on disk for every call.
Honest
Each result is classified localized / trace / aggregation, with a confidence and
advice ("aggregation-style question — read the files directly"). PASR tells the agent
when it is the wrong tool.
Zero setup
No daemon, no vector database, no index to build, offline by default.
Install
claude mcp add pasr -- uvx pasr-mcp --workspace .
Or paste this into your client's MCP config (Claude Desktop, Windsurf, Cline, Zed, …):
{ "mcpServers": { "pasr": { "command": "uvx", "args": ["pasr-mcp", "--workspace", "."] } } }
After that you never type pasr. Ask your agent a question the normal way — "how
does redirect handling work here?", "what breaks if I change HTTPAdapter?" — and the
model calls select_context / trace_dependencies itself instead of opening whole
files.
Per-client setup notes: Claude Code · Cursor · Windsurf.
Without an agent: pip install pasr-mcp, then pasr explain "<question>" prints the
same receipt the MCP tool returns. Five verbatim runs against pinned public repos are in
examples/.
In one call
One localized question — "how are redirects resolved and followed" — against
psf/requests (verbatim transcript):
PASR select_context |
agent reads the repo | |
|---|---|---|
| input tokens | 2 718 — 94% less | 42 768 |
| tool round trips | 1 | 1 large read |
| provenance | file:line + reason for all 10 spans |
none |
| wrong-tool signal | localized, confidence 0.68, "looks complete" |
— |
The selection itself runs offline in ~0.3 s on this repo — no API call, no index
build. (pasr explain prints selected in N ms to stderr; it is kept out of the
receipt, which stays wall-clock-free and byte-stable.)
Why PASR, not the usual options
| PASR | repo-map (aider) |
embedding search (claude-context, Cody) |
grep / ripgrep MCP | |
|---|---|---|---|---|
| Setup | none | none | embedder + vector DB + index build | none |
| Returns | bodies + file:line + reason + score |
signatures, no bodies | chunks, no reason | keyword hits |
| Hard token budget | never exceeded | truncates | top-k, no cap | dumps everything |
| Deterministic | yes — byte-identical | ~ | no (ANN + model drift) | yes |
| Says "wrong tool for this" | yes — routing + confidence | no | no | no |
| Audit trail | a receipt per call | no | no | no |
| Dependency-closure trace | forward + reverse (impact) | no | no | no |
On the offline bake-off (50 tasks, 10 repos, 6k-token budget, no API, no GPU),
select_context ties a full repo-map on "how does this work" questions (0.84) using
10× less text — 5.7k tokens across 18 files vs the map's 46 files of signatures — and
with map_tokens=1200 it matches repo-map overall (0.90) at 100% critical-file
coverage while still carrying real code. Full table:
docs/competitors-benchmark.md.
Does the model actually answer better?
A 50-task, 10-repo evaluation — a real model answering from only what each arm supplies, a second model judging:
select_context0.48 task success at 5.8k context tokens, vs a 59k-token whole-repo dump's 0.38 — +0.10 on paired success (pasr_fallback+0.12).- Both clear the −0.05 non-inferiority margin on the point estimate; the 95% CI still crosses it at n = 50.
- PASR gets the answer's file into context 46 / 50, vs the dump's 33 / 50.
- An agent's own grep + read-six-files scores 0.52 — but at 4× the tokens, 6 round trips, and a 30% critical-file miss.
A bounded efficiency result, not a superiority claim. Pre-registered, with the
supporting runs: eval/RESULTS.md · narrative:
docs/blog/what-worked.md.
How it works
query + file globs
→ discover safe workspace files, tokenize, line-aligned chunks
→ lossless-under-budget check: does the whole thing already fit? return it
→ candidates: BM25 (Okapi) + lexical-anchor coverage + tree-sitter symbols
+ optional sub-word / MiniLM semantic scorer
→ fuse by reciprocal-rank fusion score(s) = Σ_r 1 / (k + rank_r(s)), k = 60
→ reserve an active window (prefix + tail of the likely answer region)
→ hard-budget pack (knapsack): maximise Σ score(s) s.t. Σ tokens(s) ≤ budget
→ classify the query, score confidence, write the receipt
→ return spans + provenance + token accounting + advice
RRF needs no score calibration across the rankers — only their rank orders — so BM25, symbol hits, and the semantic scorer combine without tuning weights. The pack is whole spans only, dependency-ordered.
MCP tools
| Tool | Purpose |
|---|---|
select_context |
budgeted, provenance-tracked slice for a query — plus map_tokens (a query-ranked symbol-index header, carved out of the budget) and trace= (fold a symbol's dependency closure into the same slice) |
trace_dependencies |
deterministic def/reference closure for a symbol (Python, JS/TS); direction="callers" reverses it for impact analysis |
explain_selection |
return the stored receipt for a prior selection |
expand_context |
re-run a prior selection once with a larger budget |
CLI
pasr explain "how is the request rate limited" # run a selection, print the receipt
pasr trace enforce_per_user_request_quota src/ # a symbol's dependency closure
pasr trace HTTPAdapter src/ --callers # who calls it — impact analysis
pasr pack auth "session + login + token" src/auth/ # save a committable Context Pack
pasr review --staged src/ # touched defs + the callers they affect
pasr context --issue "$(cat issue.txt)" src/ \ # headless slice for CI / agents
--format text --metrics-file metrics.json
pasr report --price-per-mtok 3 # tokens / round trips / $ saved so far
The path is optional everywhere — with none, PASR scans the whole workspace
(.gitignore-aware). Pass a directory or globs (src/, lib/ "**/*.py") only to scope
it tighter or run faster.
Receipts land in .pasr/receipts/<id>.{json,md} (gitignored) — a byte-stable record of
what PASR handed the model and what it dropped. A usage ledger accrues in
.pasr/ledger.jsonl; pasr report turns it into "N fewer tokens across M calls, R
round trips saved". Context Packs land in .pasr/packs/ (committable) — a named,
warm-start slice the whole team loads with select_context(pack="auth"). For CI there
is a composite GitHub Action — see docs/ci.md.
Capability boundary (from the research)
- Strong: deterministic dependency / variable-trace closure — large token cuts, quality preserved or improved.
- Bounded positive: localized document / code QA — meaningfully fewer model input tokens and lower latency at parity quality.
- Do not claim: global aggregation, repo-wide code completion, lexical-mismatch position robustness. PASR classifies these and says so.
Docs
examples/— five verbatim CLI transcripts against pinned reposeval/RESULTS.md— the 50-task evaluation, pre-registered (pip install ./eval→pasr-bench)docs/competitors-benchmark.md— offline bake-off vs grep / repo-map / semantic searchdocs/architecture.md— components and data flowdocs/roadmap.md— shipped and nextCHANGELOG.md·CONTRIBUTING.md
This productises the frozen researchv2 study (model-external context optimization); a
comparative write-up is in preparation.
Development
python -m venv .venv && . .venv/bin/activate # or .venv\Scripts\Activate.ps1
pip install -e ".[dev]"
pytest -q
The pure-logic core imports no torch / transformers (and no mcp SDK — that
loads only under pasr.mcp):
python -c "import pasr.pipeline, sys; assert not {'torch','transformers'} & set(sys.modules)"
License
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pasr_mcp-0.2.0.tar.gz.
File metadata
- Download URL: pasr_mcp-0.2.0.tar.gz
- Upload date:
- Size: 96.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
30f57341c78ad370a340c3ff1988e5c92b2ee1110240930d4d8b94898bb068e1
|
|
| MD5 |
093d884baa31bc7bd9128696619e5b70
|
|
| BLAKE2b-256 |
1cb2d910d9323bdc98f19cdadfbdf49f2967cf17498115758edd1220467eeaa1
|
File details
Details for the file pasr_mcp-0.2.0-py3-none-any.whl.
File metadata
- Download URL: pasr_mcp-0.2.0-py3-none-any.whl
- Upload date:
- Size: 76.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
43b255ce6747f54f798956b4dbd3e1d521006242466e4a6ffb20eb1c3ced0e5a
|
|
| MD5 |
8dbed4bb7a23c334d185b8a869a4e6af
|
|
| BLAKE2b-256 |
9de00abff899b02796118c35aa751dff8d27af7b0c52046daec1a4a90b074404
|