@spikedpunch/mast 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/MAST_SPEC.md +2926 -0
- package/README.md +842 -0
- package/assets/skill.md +45 -0
- package/dist/ast/extract.d.ts +65 -0
- package/dist/ast/extract.d.ts.map +1 -0
- package/dist/ast/extract.js +147 -0
- package/dist/ast/extract.js.map +1 -0
- package/dist/ast/extractor.d.ts +85 -0
- package/dist/ast/extractor.d.ts.map +1 -0
- package/dist/ast/extractor.js +2 -0
- package/dist/ast/extractor.js.map +1 -0
- package/dist/ast/extractors/markdown.d.ts +40 -0
- package/dist/ast/extractors/markdown.d.ts.map +1 -0
- package/dist/ast/extractors/markdown.js +175 -0
- package/dist/ast/extractors/markdown.js.map +1 -0
- package/dist/ast/extractors/typescript.d.ts +179 -0
- package/dist/ast/extractors/typescript.d.ts.map +1 -0
- package/dist/ast/extractors/typescript.js +1439 -0
- package/dist/ast/extractors/typescript.js.map +1 -0
- package/dist/ast/parser.d.ts +5 -0
- package/dist/ast/parser.d.ts.map +1 -0
- package/dist/ast/parser.js +21 -0
- package/dist/ast/parser.js.map +1 -0
- package/dist/ast/types.d.ts +586 -0
- package/dist/ast/types.d.ts.map +1 -0
- package/dist/ast/types.js +3 -0
- package/dist/ast/types.js.map +1 -0
- package/dist/cli/docs-cmd.d.ts +22 -0
- package/dist/cli/docs-cmd.d.ts.map +1 -0
- package/dist/cli/docs-cmd.js +100 -0
- package/dist/cli/docs-cmd.js.map +1 -0
- package/dist/cli/index-cmd.d.ts +72 -0
- package/dist/cli/index-cmd.d.ts.map +1 -0
- package/dist/cli/index-cmd.js +263 -0
- package/dist/cli/index-cmd.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +4 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.d.ts +14 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +64 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/install-hooks.d.ts +3 -0
- package/dist/cli/install-hooks.d.ts.map +1 -0
- package/dist/cli/install-hooks.js +34 -0
- package/dist/cli/install-hooks.js.map +1 -0
- package/dist/cli/metrics-cmd.d.ts +3 -0
- package/dist/cli/metrics-cmd.d.ts.map +1 -0
- package/dist/cli/metrics-cmd.js +164 -0
- package/dist/cli/metrics-cmd.js.map +1 -0
- package/dist/cli/program.d.ts +14 -0
- package/dist/cli/program.d.ts.map +1 -0
- package/dist/cli/program.js +44 -0
- package/dist/cli/program.js.map +1 -0
- package/dist/cli/query.d.ts +46 -0
- package/dist/cli/query.d.ts.map +1 -0
- package/dist/cli/query.js +153 -0
- package/dist/cli/query.js.map +1 -0
- package/dist/cli/search-cmd.d.ts +33 -0
- package/dist/cli/search-cmd.d.ts.map +1 -0
- package/dist/cli/search-cmd.js +114 -0
- package/dist/cli/search-cmd.js.map +1 -0
- package/dist/cli/serve.d.ts +3 -0
- package/dist/cli/serve.d.ts.map +1 -0
- package/dist/cli/serve.js +19 -0
- package/dist/cli/serve.js.map +1 -0
- package/dist/cli/skill-install.d.ts +39 -0
- package/dist/cli/skill-install.d.ts.map +1 -0
- package/dist/cli/skill-install.js +57 -0
- package/dist/cli/skill-install.js.map +1 -0
- package/dist/cli/status.d.ts +35 -0
- package/dist/cli/status.d.ts.map +1 -0
- package/dist/cli/status.js +117 -0
- package/dist/cli/status.js.map +1 -0
- package/dist/cli/upgrade-cmd.d.ts +48 -0
- package/dist/cli/upgrade-cmd.d.ts.map +1 -0
- package/dist/cli/upgrade-cmd.js +129 -0
- package/dist/cli/upgrade-cmd.js.map +1 -0
- package/dist/cli/version.d.ts +3 -0
- package/dist/cli/version.d.ts.map +1 -0
- package/dist/cli/version.js +19 -0
- package/dist/cli/version.js.map +1 -0
- package/dist/env.d.ts +17 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +17 -0
- package/dist/env.js.map +1 -0
- package/dist/graph/checker-resolver.d.ts +156 -0
- package/dist/graph/checker-resolver.d.ts.map +1 -0
- package/dist/graph/checker-resolver.js +458 -0
- package/dist/graph/checker-resolver.js.map +1 -0
- package/dist/graph/db.d.ts +318 -0
- package/dist/graph/db.d.ts.map +1 -0
- package/dist/graph/db.js +266 -0
- package/dist/graph/db.js.map +1 -0
- package/dist/graph/index.d.ts +4 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js +5 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/graph/local-type-env.d.ts +45 -0
- package/dist/graph/local-type-env.d.ts.map +1 -0
- package/dist/graph/local-type-env.js +59 -0
- package/dist/graph/local-type-env.js.map +1 -0
- package/dist/graph/path-range.d.ts +40 -0
- package/dist/graph/path-range.d.ts.map +1 -0
- package/dist/graph/path-range.js +42 -0
- package/dist/graph/path-range.js.map +1 -0
- package/dist/graph/populate.d.ts +263 -0
- package/dist/graph/populate.d.ts.map +1 -0
- package/dist/graph/populate.js +984 -0
- package/dist/graph/populate.js.map +1 -0
- package/dist/graph/queries.d.ts +114 -0
- package/dist/graph/queries.d.ts.map +1 -0
- package/dist/graph/queries.js +415 -0
- package/dist/graph/queries.js.map +1 -0
- package/dist/graph/sqliteBatch.d.ts +48 -0
- package/dist/graph/sqliteBatch.d.ts.map +1 -0
- package/dist/graph/sqliteBatch.js +93 -0
- package/dist/graph/sqliteBatch.js.map +1 -0
- package/dist/indexer/freshness.d.ts +48 -0
- package/dist/indexer/freshness.d.ts.map +1 -0
- package/dist/indexer/freshness.js +72 -0
- package/dist/indexer/freshness.js.map +1 -0
- package/dist/indexer/import-resolver.d.ts +50 -0
- package/dist/indexer/import-resolver.d.ts.map +1 -0
- package/dist/indexer/import-resolver.js +318 -0
- package/dist/indexer/import-resolver.js.map +1 -0
- package/dist/indexer/index.d.ts +249 -0
- package/dist/indexer/index.d.ts.map +1 -0
- package/dist/indexer/index.js +560 -0
- package/dist/indexer/index.js.map +1 -0
- package/dist/indexer/walker.d.ts +60 -0
- package/dist/indexer/walker.d.ts.map +1 -0
- package/dist/indexer/walker.js +128 -0
- package/dist/indexer/walker.js.map +1 -0
- package/dist/indexer/watcher.d.ts +80 -0
- package/dist/indexer/watcher.d.ts.map +1 -0
- package/dist/indexer/watcher.js +151 -0
- package/dist/indexer/watcher.js.map +1 -0
- package/dist/mcp/context.d.ts +23 -0
- package/dist/mcp/context.d.ts.map +1 -0
- package/dist/mcp/context.js +2 -0
- package/dist/mcp/context.js.map +1 -0
- package/dist/mcp/register-tools.d.ts +21 -0
- package/dist/mcp/register-tools.d.ts.map +1 -0
- package/dist/mcp/register-tools.js +42 -0
- package/dist/mcp/register-tools.js.map +1 -0
- package/dist/mcp/server.d.ts +58 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +177 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/staleness.d.ts +72 -0
- package/dist/mcp/staleness.d.ts.map +1 -0
- package/dist/mcp/staleness.js +213 -0
- package/dist/mcp/staleness.js.map +1 -0
- package/dist/mcp/startup.d.ts +40 -0
- package/dist/mcp/startup.d.ts.map +1 -0
- package/dist/mcp/startup.js +114 -0
- package/dist/mcp/startup.js.map +1 -0
- package/dist/mcp/tools/_helpers.d.ts +26 -0
- package/dist/mcp/tools/_helpers.d.ts.map +1 -0
- package/dist/mcp/tools/_helpers.js +54 -0
- package/dist/mcp/tools/_helpers.js.map +1 -0
- package/dist/mcp/tools/callers.d.ts +4 -0
- package/dist/mcp/tools/callers.d.ts.map +1 -0
- package/dist/mcp/tools/callers.js +112 -0
- package/dist/mcp/tools/callers.js.map +1 -0
- package/dist/mcp/tools/dependencies.d.ts +4 -0
- package/dist/mcp/tools/dependencies.d.ts.map +1 -0
- package/dist/mcp/tools/dependencies.js +36 -0
- package/dist/mcp/tools/dependencies.js.map +1 -0
- package/dist/mcp/tools/efficiency.d.ts +4 -0
- package/dist/mcp/tools/efficiency.d.ts.map +1 -0
- package/dist/mcp/tools/efficiency.js +39 -0
- package/dist/mcp/tools/efficiency.js.map +1 -0
- package/dist/mcp/tools/exports.d.ts +4 -0
- package/dist/mcp/tools/exports.d.ts.map +1 -0
- package/dist/mcp/tools/exports.js +56 -0
- package/dist/mcp/tools/exports.js.map +1 -0
- package/dist/mcp/tools/implementors.d.ts +4 -0
- package/dist/mcp/tools/implementors.d.ts.map +1 -0
- package/dist/mcp/tools/implementors.js +47 -0
- package/dist/mcp/tools/implementors.js.map +1 -0
- package/dist/mcp/tools/project-skeleton.d.ts +4 -0
- package/dist/mcp/tools/project-skeleton.d.ts.map +1 -0
- package/dist/mcp/tools/project-skeleton.js +61 -0
- package/dist/mcp/tools/project-skeleton.js.map +1 -0
- package/dist/mcp/tools/reindex.d.ts +14 -0
- package/dist/mcp/tools/reindex.d.ts.map +1 -0
- package/dist/mcp/tools/reindex.js +32 -0
- package/dist/mcp/tools/reindex.js.map +1 -0
- package/dist/mcp/tools/rename-impact.d.ts +4 -0
- package/dist/mcp/tools/rename-impact.d.ts.map +1 -0
- package/dist/mcp/tools/rename-impact.js +124 -0
- package/dist/mcp/tools/rename-impact.js.map +1 -0
- package/dist/mcp/tools/search.d.ts +4 -0
- package/dist/mcp/tools/search.d.ts.map +1 -0
- package/dist/mcp/tools/search.js +69 -0
- package/dist/mcp/tools/search.js.map +1 -0
- package/dist/mcp/tools/signature.d.ts +4 -0
- package/dist/mcp/tools/signature.d.ts.map +1 -0
- package/dist/mcp/tools/signature.js +127 -0
- package/dist/mcp/tools/signature.js.map +1 -0
- package/dist/mcp/tools/status.d.ts +4 -0
- package/dist/mcp/tools/status.d.ts.map +1 -0
- package/dist/mcp/tools/status.js +30 -0
- package/dist/mcp/tools/status.js.map +1 -0
- package/dist/search/declex.d.ts +109 -0
- package/dist/search/declex.d.ts.map +1 -0
- package/dist/search/declex.js +242 -0
- package/dist/search/declex.js.map +1 -0
- package/dist/search/fts.d.ts +77 -0
- package/dist/search/fts.d.ts.map +1 -0
- package/dist/search/fts.js +223 -0
- package/dist/search/fts.js.map +1 -0
- package/dist/search/fused.d.ts +110 -0
- package/dist/search/fused.d.ts.map +1 -0
- package/dist/search/fused.js +323 -0
- package/dist/search/fused.js.map +1 -0
- package/dist/search/potential-matches.d.ts +88 -0
- package/dist/search/potential-matches.d.ts.map +1 -0
- package/dist/search/potential-matches.js +83 -0
- package/dist/search/potential-matches.js.map +1 -0
- package/dist/search/scope.d.ts +66 -0
- package/dist/search/scope.d.ts.map +1 -0
- package/dist/search/scope.js +71 -0
- package/dist/search/scope.js.map +1 -0
- package/dist/store/config.d.ts +57 -0
- package/dist/store/config.d.ts.map +1 -0
- package/dist/store/config.js +208 -0
- package/dist/store/config.js.map +1 -0
- package/dist/store/lock.d.ts +101 -0
- package/dist/store/lock.d.ts.map +1 -0
- package/dist/store/lock.js +182 -0
- package/dist/store/lock.js.map +1 -0
- package/dist/store/lockMetrics.d.ts +52 -0
- package/dist/store/lockMetrics.d.ts.map +1 -0
- package/dist/store/lockMetrics.js +29 -0
- package/dist/store/lockMetrics.js.map +1 -0
- package/dist/store/sqliteChunkStore.d.ts +44 -0
- package/dist/store/sqliteChunkStore.d.ts.map +1 -0
- package/dist/store/sqliteChunkStore.js +135 -0
- package/dist/store/sqliteChunkStore.js.map +1 -0
- package/dist/telemetry/lockMetricsSummary.d.ts +42 -0
- package/dist/telemetry/lockMetricsSummary.d.ts.map +1 -0
- package/dist/telemetry/lockMetricsSummary.js +139 -0
- package/dist/telemetry/lockMetricsSummary.js.map +1 -0
- package/dist/telemetry/metrics.d.ts +140 -0
- package/dist/telemetry/metrics.d.ts.map +1 -0
- package/dist/telemetry/metrics.js +312 -0
- package/dist/telemetry/metrics.js.map +1 -0
- package/dist/telemetry/tokenizer.d.ts +125 -0
- package/dist/telemetry/tokenizer.d.ts.map +1 -0
- package/dist/telemetry/tokenizer.js +174 -0
- package/dist/telemetry/tokenizer.js.map +1 -0
- package/package.json +81 -0
package/README.md
ADDED
|
@@ -0,0 +1,842 @@
|
|
|
1
|
+
# MAST — Monorepo AST Search Tool
|
|
2
|
+
|
|
3
|
+
MAST is a code-search engine that runs as either an MCP server (for AI assistants) or a standalone CLI. It parses TypeScript and JavaScript source files with a real AST parser (`tree-sitter`), stores the resulting symbol graph and code chunks in SQLite, and answers queries with lexical BM25 search fused against a declaration-exact ranker via Reciprocal Rank Fusion.
|
|
4
|
+
|
|
5
|
+
The core design principle: **return exactly the code an assistant needs, nothing more**. Rather than reading entire files, MAST returns the specific function, interface, or type declaration that matches a query — saving tokens, reducing context noise, and letting AI tools navigate large codebases without drowning in irrelevant content.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Contents
|
|
10
|
+
|
|
11
|
+
- [Why MAST?](#why-mast)
|
|
12
|
+
- [Requirements](#requirements)
|
|
13
|
+
- [Install](#install)
|
|
14
|
+
- [Quick Start](#quick-start)
|
|
15
|
+
- [Use it from your AI assistant](#use-it-from-your-ai-assistant)
|
|
16
|
+
- [Upgrading](#upgrading)
|
|
17
|
+
- [Using MAST in a monorepo](#using-mast-in-a-monorepo)
|
|
18
|
+
- [CLI Reference](#cli-reference)
|
|
19
|
+
- [MCP Tool Reference](#mcp-tool-reference)
|
|
20
|
+
- [Configuration](#configuration)
|
|
21
|
+
- [How It Works](#how-it-works)
|
|
22
|
+
- [Token Efficiency](#token-efficiency)
|
|
23
|
+
- [History](#history)
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Why MAST?
|
|
28
|
+
|
|
29
|
+
When an AI assistant needs to understand code, the naive approach is to read full files. This wastes tokens (most of a 200-line file is irrelevant to the question), inflates context windows, and forces the model to filter signal from noise on every call.
|
|
30
|
+
|
|
31
|
+
MAST takes a different approach:
|
|
32
|
+
|
|
33
|
+
- **AST-level chunking** — every function, class, interface, and type alias is its own chunk. The assistant gets the exact declaration it needs, not the file it happens to live in.
|
|
34
|
+
- **Ranked search** — BM25 (FTS5) handles keyword and identifier queries; a declaration-exact ranker ("ranker D") catches exact-symbol-name queries that BM25's trigram tokenizer can rank inconsistently. Both are fused via Reciprocal Rank Fusion so a chunk that both rankers agree on outranks one that only one of them found.
|
|
35
|
+
- **Structural queries** — "who calls this function?", "what implements this interface?", "what does this file import?" are answered from a pre-built symbol graph, not by grepping source. Answers are instantaneous and structurally correct.
|
|
36
|
+
- **JIT staleness detection** — on every read, MAST checks whether the file on disk has changed since it was last indexed. If it has, the file is transparently re-parsed in the background before the result is returned. The index never goes stale without the assistant knowing.
|
|
37
|
+
- **Token accounting** — every tool response includes `_stats` with the token count returned and the counterfactual "what would a naive full-file read have cost?", giving a concrete measure of efficiency over time.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Requirements
|
|
42
|
+
|
|
43
|
+
- **Node.js ≥ 22** (this repo pins the version it develops against in `.nvmrc`)
|
|
44
|
+
- **A C++ toolchain**, for the two native modules (`better-sqlite3`, `tree-sitter`).
|
|
45
|
+
Prebuilt binaries cover most platforms; when none matches your Node ABI, `node-gyp`
|
|
46
|
+
builds from source and needs:
|
|
47
|
+
- **macOS** — `xcode-select --install`
|
|
48
|
+
- **Debian/Ubuntu** — `sudo apt install build-essential python3`
|
|
49
|
+
- **Windows** — install the "Desktop development with C++" workload from Visual Studio
|
|
50
|
+
Build Tools
|
|
51
|
+
|
|
52
|
+
No services, no API keys, no network at query time. Everything is local SQLite.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Install
|
|
57
|
+
|
|
58
|
+
As a dev dependency of the project you want to index — recommended, because the version
|
|
59
|
+
is then pinned in your lockfile alongside everything else:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pnpm add -D @spikedpunch/mast # or: npm i -D / yarn add -D
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Or globally, if you want one `mast` across many checkouts:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pnpm add -g @spikedpunch/mast
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Verify:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
mast --version
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Quick Start
|
|
80
|
+
|
|
81
|
+
Three commands from nothing to a searchable index:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
cd /path/to/your/project
|
|
85
|
+
|
|
86
|
+
mast init # write .mast/, then run the first full index
|
|
87
|
+
mast status # confirm it is fresh
|
|
88
|
+
mast search "createUser" # search it
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`mast search` prints the matching declaration, not the file it lives in:
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
$ mast search "compareVersions" -n 1
|
|
95
|
+
src/cli/upgrade-cmd.ts:39 compareVersions function (exported)
|
|
96
|
+
/** Semver compare, prerelease-aware. Returns <0, 0, or >0. */
|
|
97
|
+
export function compareVersions(a: string, b: string): number {
|
|
98
|
+
...
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
270 tokens returned vs 2140 to read the files whole — 87% saved
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The last line is real accounting, not a slogan: every response carries `_stats` with what
|
|
105
|
+
it returned and an upper bound on reading the referenced files whole. On a small file the
|
|
106
|
+
saving can be *negative*, and MAST says so rather than rounding it into a win.
|
|
107
|
+
|
|
108
|
+
An answer that MAST cannot fully stand behind says so, on the same surface that shows the
|
|
109
|
+
result. A file edited since it was indexed is marked, because the body printed under it is
|
|
110
|
+
the *old* one:
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
! 1 of 2 results are from files that changed since indexing —
|
|
114
|
+
the code shown below may be out of date. Run `mast index` to refresh.
|
|
115
|
+
|
|
116
|
+
src/a.ts:1 alphaFunction function (exported) [STALE]
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
And an empty answer distinguishes the two reasons it can be empty:
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
$ mast search "kept_symbol"
|
|
123
|
+
no matches (mast indexes TypeScript, JavaScript, and Markdown only —
|
|
124
|
+
a symbol in any other language is invisible to it, not absent from the repo)
|
|
125
|
+
|
|
126
|
+
$ mast search "anything" # in a directory with no index
|
|
127
|
+
nothing is indexed at this path — this is not evidence the symbol is absent.
|
|
128
|
+
run `mast index` first, or check `mast status` for the path being used.
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Narrow it with `--type`, `--language`, `--exported`, `--file`, `-n`:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
mast search "greet" --type method --exported -n 5
|
|
135
|
+
mast search "config" --file "src/store/**"
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Keep it current as you work — or let a git hook do it:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
mast index --incremental # reindex only what changed
|
|
142
|
+
mast install-hooks # reindex automatically after commits and checkouts
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Everything shipped with your build is readable offline, so you never have to work out
|
|
146
|
+
which docs match your version:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
mast docs # list the topics
|
|
150
|
+
mast docs spec # the full behavioural specification
|
|
151
|
+
mast skill # the instructions to paste into an agent prompt
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Use it from your AI assistant
|
|
157
|
+
|
|
158
|
+
MAST speaks MCP over stdio. `mast serve` is the server command; the configuration below
|
|
159
|
+
differs only in where each tool keeps its config file.
|
|
160
|
+
|
|
161
|
+
If you installed MAST as a dev dependency rather than globally, replace `mast` with
|
|
162
|
+
`npx @spikedpunch/mast` (or `pnpm exec mast`) in any of these.
|
|
163
|
+
|
|
164
|
+
### Claude Code
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
claude mcp add mast -- mast serve
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Add `--scope project` to write `.mcp.json` into the repository so your team picks it up
|
|
171
|
+
from the checkout.
|
|
172
|
+
|
|
173
|
+
### Claude Desktop
|
|
174
|
+
|
|
175
|
+
`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS,
|
|
176
|
+
`%APPDATA%\Claude\claude_desktop_config.json` on Windows:
|
|
177
|
+
|
|
178
|
+
```json
|
|
179
|
+
{
|
|
180
|
+
"mcpServers": {
|
|
181
|
+
"mast": {
|
|
182
|
+
"command": "mast",
|
|
183
|
+
"args": ["serve"],
|
|
184
|
+
"env": { "MAST_STATE_DIR": "/absolute/path/to/your/project/.mast" }
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Claude Desktop does not run in your project directory, so `MAST_STATE_DIR` must be
|
|
191
|
+
absolute. The CLI and editor integrations below infer it from the working directory.
|
|
192
|
+
|
|
193
|
+
### Cursor
|
|
194
|
+
|
|
195
|
+
`.cursor/mcp.json` in the project, or `~/.cursor/mcp.json` globally:
|
|
196
|
+
|
|
197
|
+
```json
|
|
198
|
+
{
|
|
199
|
+
"mcpServers": {
|
|
200
|
+
"mast": { "command": "mast", "args": ["serve"] }
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### VS Code (GitHub Copilot)
|
|
206
|
+
|
|
207
|
+
`.vscode/mcp.json`:
|
|
208
|
+
|
|
209
|
+
```json
|
|
210
|
+
{
|
|
211
|
+
"servers": {
|
|
212
|
+
"mast": { "type": "stdio", "command": "mast", "args": ["serve"] }
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Windsurf
|
|
218
|
+
|
|
219
|
+
`~/.codeium/windsurf/mcp_config.json`:
|
|
220
|
+
|
|
221
|
+
```json
|
|
222
|
+
{
|
|
223
|
+
"mcpServers": {
|
|
224
|
+
"mast": { "command": "mast", "args": ["serve"] }
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Zed
|
|
230
|
+
|
|
231
|
+
`settings.json`:
|
|
232
|
+
|
|
233
|
+
```json
|
|
234
|
+
{
|
|
235
|
+
"context_servers": {
|
|
236
|
+
"mast": { "command": { "path": "mast", "args": ["serve"] } }
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Any other MCP client
|
|
242
|
+
|
|
243
|
+
Run `mast serve` over stdio from the project root. It advertises eleven read tools and
|
|
244
|
+
needs no arguments beyond `serve`.
|
|
245
|
+
|
|
246
|
+
### Tell the assistant how to use it
|
|
247
|
+
|
|
248
|
+
Registering the server gives the model the tools; it does not tell it *when* to reach for
|
|
249
|
+
them, or how to read a flagged answer. `mast skill` prints instructions written for that —
|
|
250
|
+
paste them into your system prompt, `CLAUDE.md`, `.cursorrules`, or a skill file:
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
mast skill # print it
|
|
254
|
+
mast skill --install # splice it into this project's agent config files
|
|
255
|
+
mast skill --install --dry-run
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
`--install` writes only into files that **already exist** — `CLAUDE.md`, `AGENTS.md`,
|
|
259
|
+
`.cursorrules`, `.windsurfrules`, `.github/copilot-instructions.md` — and writes inside a
|
|
260
|
+
marked block, so re-running after an upgrade replaces the previous copy instead of adding
|
|
261
|
+
a second one. It never runs on its own, and it never creates a config file you did not
|
|
262
|
+
already keep.
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Upgrading
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
mast upgrade
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
This checks for a newer release and prints the exact command for how *you* installed it —
|
|
273
|
+
it does not upgrade in place, because a CLI cannot reliably tell a global install from a
|
|
274
|
+
dev dependency, and guessing wrong runs the wrong command in your repository.
|
|
275
|
+
|
|
276
|
+
More importantly, it tells you the one thing your package manager cannot: whether the
|
|
277
|
+
upgrade changes the **index schema**. When it does, MAST discards the index and rebuilds
|
|
278
|
+
it on the next `serve` or `index`. Nothing is lost that cannot be rebuilt — the index is
|
|
279
|
+
derived state — but on a large monorepo it is minutes, and it is better known in advance
|
|
280
|
+
than discovered as an unexplained stall.
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## Using MAST in a monorepo
|
|
285
|
+
|
|
286
|
+
**One index at the repository root** is usually right. Cross-package imports resolve, so
|
|
287
|
+
`mast_callers` finds callers in sibling packages — which is the reason to use a monorepo
|
|
288
|
+
tool rather than one index per package.
|
|
289
|
+
|
|
290
|
+
**What is indexed.** `.ts`, `.tsx`, `.js`, `.jsx`, and `.md`, minus `node_modules`,
|
|
291
|
+
`dist`, `build`, `coverage`, `.next`, `.turbo`, `.mast`, and test files. Override with
|
|
292
|
+
`--extensions` and `--exclude` on `mast init`, or edit `.mast/config.json`.
|
|
293
|
+
|
|
294
|
+
**Other languages are not indexed, and this matters.** MAST parses TypeScript and
|
|
295
|
+
JavaScript only. A symbol defined in Python, Go, Java, or Rust is absent from the index,
|
|
296
|
+
which looks exactly like absent from the repository. Treat an empty result as "MAST did
|
|
297
|
+
not find it", never as "it does not exist" — `mast skill` says this to the model too.
|
|
298
|
+
|
|
299
|
+
**Add `.mast/` to `.gitignore`.** It is derived state, it is large, and it is
|
|
300
|
+
machine-specific.
|
|
301
|
+
|
|
302
|
+
**A custom index location is not remembered between runs.** `--state-dir` applies to the
|
|
303
|
+
one command you pass it to. Path settings are deliberately never read back out of a
|
|
304
|
+
persisted config — an absolute path written by a previous run (or a previous container)
|
|
305
|
+
can resolve somewhere that no longer exists, or worse, somewhere belonging to a different
|
|
306
|
+
project. To make a custom location stick, put it in source control or the environment:
|
|
307
|
+
|
|
308
|
+
```json
|
|
309
|
+
// mast.config.json, at the project root
|
|
310
|
+
{ "state_dir": ".cache/mast" }
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
export MAST_STATE_DIR=/absolute/path/to/index
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Resolution order is `--state-dir` → `MAST_STATE_DIR` → `mast.config.json` → `.mast`.
|
|
318
|
+
`mast status` prints the directory it resolved, and says so plainly when nothing has been
|
|
319
|
+
indexed there.
|
|
320
|
+
|
|
321
|
+
**Scale.** A cold index of VS Code — 8,653 files, 152,969 chunks — takes about two
|
|
322
|
+
minutes and produces a 794 MB state directory. Incremental reindexing of a changed file
|
|
323
|
+
is milliseconds.
|
|
324
|
+
## CLI Reference
|
|
325
|
+
|
|
326
|
+
### `mast init [path]`
|
|
327
|
+
|
|
328
|
+
Initialise MAST for a project and run the initial full index.
|
|
329
|
+
|
|
330
|
+
```
|
|
331
|
+
Options:
|
|
332
|
+
--state-dir <dir> Where to write index state (default: <path>/.mast)
|
|
333
|
+
--extensions <ext,...> File extensions to index (default: .ts,.tsx,.js,.jsx,.md)
|
|
334
|
+
--exclude <pattern,...> Glob patterns to exclude
|
|
335
|
+
--no-index Create config only; skip initial indexing
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
**Why:** Creates the state directory structure, writes `config.json`, and runs a full parse + symbol extraction pass. Running this once upfront means subsequent incremental runs only touch changed files.
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
### `mast search <query> [path]`
|
|
343
|
+
|
|
344
|
+
Search the index and print readable results.
|
|
345
|
+
|
|
346
|
+
```
|
|
347
|
+
Options:
|
|
348
|
+
-n, --limit <n> Max results, 1-50 (default: 10)
|
|
349
|
+
-t, --type <kind> function | method | class_shell | interface | type | export | block | doc
|
|
350
|
+
-l, --language <lang> typescript | javascript | markdown
|
|
351
|
+
-e, --exported Only exported symbols
|
|
352
|
+
-f, --file <glob> Restrict to files matching a glob
|
|
353
|
+
--state-dir <dir> State directory
|
|
354
|
+
--json Emit the raw MCP response instead of text
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
**Why:** the fastest way to check what the index actually contains, and the same code path
|
|
358
|
+
the MCP `mast_search` tool uses — it dispatches through the registered handler rather than
|
|
359
|
+
re-implementing ranking, so CLI and assistant results cannot disagree. Staleness and
|
|
360
|
+
truncation flags are printed above the results; an empty result that is empty *because* the
|
|
361
|
+
index was busy says so.
|
|
362
|
+
|
|
363
|
+
For scripting, `mast query mast_search '{...}'` gives byte-identical MCP output.
|
|
364
|
+
|
|
365
|
+
---
|
|
366
|
+
|
|
367
|
+
### `mast index [path]`
|
|
368
|
+
|
|
369
|
+
Build or update the index.
|
|
370
|
+
|
|
371
|
+
```
|
|
372
|
+
Options:
|
|
373
|
+
--state-dir <dir> State directory
|
|
374
|
+
--incremental Only reindex files changed since last run
|
|
375
|
+
--show-progress Print indexing progress to stderr
|
|
376
|
+
--checker Opt-in TypeScript-checker pass: upgrades heuristic potential_matches
|
|
377
|
+
into verified caller edges (or drops non-call-site noise). Can take
|
|
378
|
+
tens of seconds on a large monorepo — not part of the default path.
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
**Why incremental:** The incremental path diffs the current file manifest against stored mtimes. Only stale, added, or deleted files are processed — for a large codebase this cuts index time from seconds to milliseconds on most runs.
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
### `mast serve`
|
|
386
|
+
|
|
387
|
+
Start the MCP server over stdio.
|
|
388
|
+
|
|
389
|
+
```
|
|
390
|
+
Options:
|
|
391
|
+
--state-dir <dir> State directory
|
|
392
|
+
--no-startup-reindex Skip the startup staleness check (not recommended)
|
|
393
|
+
--watch Watch source files and incrementally reindex on change
|
|
394
|
+
(interactive use; not needed in the container ladder)
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
The server implements a four-step startup ladder so MCP clients get a usable server in under a second even for large projects. See [Startup Ladder](#startup-ladder) for details.
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
### `mast status [path]`
|
|
402
|
+
|
|
403
|
+
Print index health.
|
|
404
|
+
|
|
405
|
+
```
|
|
406
|
+
Options:
|
|
407
|
+
--state-dir <dir> State directory
|
|
408
|
+
--json Output as JSON
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
Reports `last_indexed`, `indexed_files`, `chunk_count`, `stale_files`, `parse_errors`, `write_errors`, `index_fresh`, and `freshness_cause`. Use this to diagnose why search results look outdated.
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
### `mast metrics [path]`
|
|
416
|
+
|
|
417
|
+
Show token-efficiency metrics.
|
|
418
|
+
|
|
419
|
+
```
|
|
420
|
+
Options:
|
|
421
|
+
--since <window> Time window: 7d, 24h, 30m (default: 7d)
|
|
422
|
+
--rollup Collapse raw rows older than --keep-days into daily roll-ups
|
|
423
|
+
--vacuum Delete daily roll-up rows older than --keep-days
|
|
424
|
+
--keep-days <n> Retention days (default: 7 for rollup, 90 for vacuum)
|
|
425
|
+
--state-dir <dir> State directory
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
Prints a column-aligned table: tool name, call count, tokens returned, average duration, and efficiency ratio. Use `--rollup` + `--vacuum` periodically to keep the metrics database from growing unbounded.
|
|
429
|
+
|
|
430
|
+
---
|
|
431
|
+
|
|
432
|
+
### `mast install-hooks [path]`
|
|
433
|
+
|
|
434
|
+
Install git `post-commit` / `post-checkout` hooks that run `mast index --incremental` automatically, so the index stays fresh across commits and branch switches without a manual step.
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
### `mast query <tool> [json] [path]`
|
|
439
|
+
|
|
440
|
+
Invoke any MCP read tool directly, with byte-identical output to the MCP transport.
|
|
441
|
+
|
|
442
|
+
```
|
|
443
|
+
Options:
|
|
444
|
+
--state-dir <dir> State directory
|
|
445
|
+
--json Emit the exact single-line MCP response (default pretty-prints)
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
```bash
|
|
449
|
+
mast query mast_callers '{"symbol":"resolveConfig"}'
|
|
450
|
+
mast query mast_project_skeleton '{}'
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
**Why:** the scripting and debugging surface. `mast search` is the readable front door to
|
|
454
|
+
one tool; this reaches all eleven, and returns exactly what an assistant would receive —
|
|
455
|
+
so a disagreement between what you see and what the model saw is not possible. Naming a
|
|
456
|
+
tool that does not exist lists the ones that do.
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
460
|
+
### `mast docs [topic]`
|
|
461
|
+
|
|
462
|
+
Print documentation shipped with the installed build — `readme`, `spec`, or `skill`. No
|
|
463
|
+
argument lists the topics with the version they belong to.
|
|
464
|
+
|
|
465
|
+
**Why:** removes the step where a reader looks up their version and then finds docs for a
|
|
466
|
+
different one. What `mast docs` prints is what the binary in your `node_modules` does.
|
|
467
|
+
|
|
468
|
+
---
|
|
469
|
+
|
|
470
|
+
### `mast skill [path]`
|
|
471
|
+
|
|
472
|
+
Print the MAST instructions to paste into an agent prompt, `CLAUDE.md`, `.cursorrules`, or
|
|
473
|
+
a skill file.
|
|
474
|
+
|
|
475
|
+
```
|
|
476
|
+
Options:
|
|
477
|
+
--install Splice into this project's existing agent config files
|
|
478
|
+
--dry-run With --install, report what would change without writing
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
**Why:** registering the MCP server gives a model the tools but not the judgement — when to
|
|
482
|
+
search instead of reading, that code tokens beat prose in a query, and how to read a
|
|
483
|
+
staleness or truncation flag. It also tells the model that an empty result means "MAST did
|
|
484
|
+
not find it", not "it does not exist", which is the single most consequential thing to get
|
|
485
|
+
right about a search tool.
|
|
486
|
+
|
|
487
|
+
---
|
|
488
|
+
|
|
489
|
+
### `mast upgrade [path]`
|
|
490
|
+
|
|
491
|
+
Check for a newer release; print how to install it, and what it will cost.
|
|
492
|
+
|
|
493
|
+
**Why:** it detects how MAST was installed and prints the matching command rather than
|
|
494
|
+
running it, because a CLI cannot reliably distinguish a global install from a dev
|
|
495
|
+
dependency. It also reports whether the upgrade bumps the index schema — which forces a
|
|
496
|
+
full reindex on the next `serve` — and your package manager cannot tell you that.
|
|
497
|
+
|
|
498
|
+
---
|
|
499
|
+
|
|
500
|
+
## MCP Tool Reference
|
|
501
|
+
|
|
502
|
+
MAST registers 11 tools with the MCP server. Every read tool includes a `_stats` block:
|
|
503
|
+
|
|
504
|
+
```typescript
|
|
505
|
+
{
|
|
506
|
+
tool: string,
|
|
507
|
+
tokens_returned: number,
|
|
508
|
+
tokens_full_file_upper_bound: number,
|
|
509
|
+
files_referenced: string[],
|
|
510
|
+
efficiency_ratio: number, // 1 - (returned / full_file)
|
|
511
|
+
duration_ms: number,
|
|
512
|
+
}
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
---
|
|
516
|
+
|
|
517
|
+
### `mast_search`
|
|
518
|
+
|
|
519
|
+
Lexical BM25 + declaration-exact search over the indexed codebase.
|
|
520
|
+
|
|
521
|
+
```typescript
|
|
522
|
+
{
|
|
523
|
+
query: string, // natural language or identifier
|
|
524
|
+
limit?: number, // max results (default 10, max 50)
|
|
525
|
+
language?: "typescript" | "javascript" | "markdown" | null,
|
|
526
|
+
file_pattern?: string | null, // glob: "src/api/**"
|
|
527
|
+
chunk_type?: "function" | "method" | "class_shell" | "interface" | "type" | "export" | "block" | "doc" | null,
|
|
528
|
+
only_exported?: boolean
|
|
529
|
+
}
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
**Returns:** `{ results[], suggestions?, _stats }`. Each result includes `file_path`, `start_line`, `end_line`, `content`, `chunk_type`, `symbol_name`, `parent_symbol`, `is_exported`, `match_score` (BM25 score, negative; `null` when the hit came only from ranker D), `rank`, `match_snippet`, and an optional `related` hint when a method and its class shell both matched (only the higher-ranked one is returned). `suggestions` is present, possibly empty, only when `results` is empty — a zero-result "did you mean" assist.
|
|
533
|
+
|
|
534
|
+
**Why:** `grep` and `glob` find exact strings and require the caller to already know the pattern. `mast_search` ranks by relevance across two signals fused with Reciprocal Rank Fusion:
|
|
535
|
+
|
|
536
|
+
- **BM25 (FTS5, trigram-tokenized)** — the general-purpose lexical ranker; handles keyword queries and sub-token/camelCase matches.
|
|
537
|
+
- **Ranker D (declaration-exact)** — a direct match against a chunk's own `symbol_name` (full name or final dot-segment, case-insensitive). Catches exact-symbol queries BM25's trigram scoring can under-rank. Gated by the `declaration_exact_ranker` config key (default on); when off, `mast_search` is BM25-only.
|
|
538
|
+
|
|
539
|
+
A chunk both rankers agree on outranks one only one of them found. `file_pattern` and `language` bound the pool **both** rankers draw from, so a scoped search never returns a file outside the scope. `file_pattern` is a glob matched with the same primitive that applies `exclude_patterns` at index time: `*` does not cross `/`, `**` does, `?` is one non-`/` character, matching is case-sensitive, and everything else — `.`, `_`, `-` — is literal.
|
|
540
|
+
|
|
541
|
+
---
|
|
542
|
+
|
|
543
|
+
### `mast_project_skeleton`
|
|
544
|
+
|
|
545
|
+
All exported symbols grouped by file, optionally scoped to a directory.
|
|
546
|
+
|
|
547
|
+
```typescript
|
|
548
|
+
{
|
|
549
|
+
directory?: string | null, // path prefix: "src/api"
|
|
550
|
+
max_depth?: number, // max subdirectory depth (default unlimited)
|
|
551
|
+
file_pattern?: string | null // glob filter on file paths
|
|
552
|
+
}
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
**Returns:** `{ files: [{ file_path, exports: string[] }], _stats }`.
|
|
556
|
+
|
|
557
|
+
**Why:** Before navigating a codebase, an assistant needs orientation — "what exists here?". Reading every file to find its exports is wasteful. `mast_project_skeleton` returns a directory-scoped map of file → exported names in a single call, letting an assistant build a mental model of a subsystem without opening any files.
|
|
558
|
+
|
|
559
|
+
---
|
|
560
|
+
|
|
561
|
+
### `mast_exports`
|
|
562
|
+
|
|
563
|
+
All exported symbols from a single file with type signatures and TSDoc.
|
|
564
|
+
|
|
565
|
+
```typescript
|
|
566
|
+
{
|
|
567
|
+
file_path: string // relative to project root
|
|
568
|
+
}
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
**Returns:** `{ file_path, exports: [{ name, kind, signature, line, doc }], _stats }`.
|
|
572
|
+
|
|
573
|
+
**Why:** The natural follow-up to `mast_project_skeleton`. Once an assistant knows which file is relevant, `mast_exports` gives full signatures without the function bodies — enough to understand the public surface of a module without paying for the implementation.
|
|
574
|
+
|
|
575
|
+
Methods are intentionally omitted (they surface via `mast_signature` on their parent class), so the result stays focused on the module's public contract.
|
|
576
|
+
|
|
577
|
+
---
|
|
578
|
+
|
|
579
|
+
### `mast_signature`
|
|
580
|
+
|
|
581
|
+
Declaration, TSDoc, and resolved parameter type context for a named symbol.
|
|
582
|
+
|
|
583
|
+
```typescript
|
|
584
|
+
{
|
|
585
|
+
symbol: string, // e.g. "handleLogin", "AuthService"
|
|
586
|
+
file_path?: string | null // narrow to a specific file
|
|
587
|
+
}
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
**Returns:** An array of `SignatureResult`, each with `symbol`, `file_path`, `line`, `signature`, `doc`, `params`, `return_type`, and `type_context`.
|
|
591
|
+
|
|
592
|
+
`type_context` is automatically populated: user-defined PascalCase type names appearing in the signature are resolved to their own signatures via a three-priority lookup — same file first, then named imports, then a global exported-type fallback. Long signatures are truncated at 500 characters. This means a single `mast_signature` call gives the assistant the full type picture for a function without needing separate lookups.
|
|
593
|
+
|
|
594
|
+
**Why:** When an assistant sees `function processOrder(order: Order, ctx: RequestContext): Promise<Result>`, knowing the signature of `Order`, `RequestContext`, and `Result` is essential for understanding what the function does. Rather than making three more tool calls, `mast_signature` resolves them inline.
|
|
595
|
+
|
|
596
|
+
---
|
|
597
|
+
|
|
598
|
+
### `mast_callers`
|
|
599
|
+
|
|
600
|
+
Who calls a given symbol, split into verified callers (from the symbol graph) and potential matches (from full-text identifier search).
|
|
601
|
+
|
|
602
|
+
```typescript
|
|
603
|
+
{
|
|
604
|
+
symbol: string,
|
|
605
|
+
file_path?: string | null,
|
|
606
|
+
transitive?: boolean, // walk the full call chain (default false)
|
|
607
|
+
include_potential?: boolean // include identifier_fts matches (default true)
|
|
608
|
+
}
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
**Returns:** `{ verified_callers[], potential_matches[], summary: { verified_count, potential_count, transitive, checker_classified_non_call_site, checker_classified_different_declaration }, _stats }`.
|
|
612
|
+
|
|
613
|
+
**Why:** Impact analysis before a refactor requires knowing who depends on a symbol. Verified callers are graph-resolved (definitive, no false positives from name collisions). Potential matches are identifier-FTS hits where the call wasn't statically resolvable — they may be false positives but are worth reviewing. Separating the two lets the assistant reason about confidence: if `verified_count` is 3 and `potential_count` is 0, the refactor scope is well-understood. If `potential_count` is 15, there's more uncertainty. Running `mast index --checker` upgrades some potential matches to verified edges (or drops non-call-site noise) — the `checker_classified_*` counts report how many, and are 0 when the checker pass has never run.
|
|
614
|
+
|
|
615
|
+
---
|
|
616
|
+
|
|
617
|
+
### `mast_dependencies`
|
|
618
|
+
|
|
619
|
+
All imports recorded for a file.
|
|
620
|
+
|
|
621
|
+
```typescript
|
|
622
|
+
{
|
|
623
|
+
file_path: string
|
|
624
|
+
}
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
**Returns:** `{ file_path, imports: [{ module, symbols[], is_external, resolved_path? }], _stats }`.
|
|
628
|
+
|
|
629
|
+
**Why:** Understanding a file's dependency surface is the first step in reasoning about what it does. External imports (no `resolved_path`) are flagged so the assistant knows the resolution boundary. Internal imports include the resolved path so callers can follow the chain.
|
|
630
|
+
|
|
631
|
+
---
|
|
632
|
+
|
|
633
|
+
### `mast_implementors`
|
|
634
|
+
|
|
635
|
+
All concrete classes that implement a given interface, with their method lists.
|
|
636
|
+
|
|
637
|
+
```typescript
|
|
638
|
+
{
|
|
639
|
+
interface_name: string
|
|
640
|
+
}
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
**Returns:** `{ results: [{ class_name, file_path, line, methods[] }], _stats }`.
|
|
644
|
+
|
|
645
|
+
**Why:** In a dependency-injection codebase, `interface_name → implementors` is the answer to "what actually runs here?". Rather than grepping for `implements InterfaceName`, MAST stores explicit `IMPLEMENTS` edges in the graph at index time, making the lookup instantaneous and structurally correct.
|
|
646
|
+
|
|
647
|
+
---
|
|
648
|
+
|
|
649
|
+
### `mast_rename_impact`
|
|
650
|
+
|
|
651
|
+
Composed refactor checklist for renaming a symbol: declaration sites, verified callers, potential matches, and barrel re-exports, in one call.
|
|
652
|
+
|
|
653
|
+
```typescript
|
|
654
|
+
{
|
|
655
|
+
symbol: string,
|
|
656
|
+
file_path?: string | null
|
|
657
|
+
}
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
**Returns:** `{ symbol, declaration_sites[], verified_callers[], potential_matches[], barrel_exports[], summary: { declaration_count, verified_count, potential_count, barrel_count, checklist, checker_classified_non_call_site, checker_classified_different_declaration }, _stats }`.
|
|
661
|
+
|
|
662
|
+
**Why:** A rename touches more than call sites — barrel re-exports (`export { Foo } from './foo'`, possibly aliased) also need updating, and are easy to miss with a plain caller search. `mast_rename_impact` composes `mast_callers`' machinery with barrel-export detection so the assistant gets one checklist instead of three separate queries.
|
|
663
|
+
|
|
664
|
+
---
|
|
665
|
+
|
|
666
|
+
### `mast_reindex`
|
|
667
|
+
|
|
668
|
+
Trigger a synchronous reindex from within an MCP session.
|
|
669
|
+
|
|
670
|
+
```typescript
|
|
671
|
+
{
|
|
672
|
+
full?: boolean // force full reindex (default: incremental)
|
|
673
|
+
}
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
**Returns:** `{ files_indexed, files_skipped, chunks_added, chunks_removed, parse_errors, write_errors, duration_ms }`.
|
|
677
|
+
|
|
678
|
+
**Why:** Long-running editing sessions accumulate staleness — new symbols and files won't be found by `mast_search` until they're indexed (JIT staleness handling keeps *already-indexed* files' line coordinates correct on read, but can't discover a brand-new file or symbol). `mast_reindex` lets the assistant refresh the index on demand — for example, after a large refactor — without leaving the MCP session. The `full` flag is available when incremental state is suspected to be corrupt.
|
|
679
|
+
|
|
680
|
+
---
|
|
681
|
+
|
|
682
|
+
### `mast_status`
|
|
683
|
+
|
|
684
|
+
Health snapshot of the index.
|
|
685
|
+
|
|
686
|
+
```typescript
|
|
687
|
+
// no inputs
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
**Returns:** `{ state_dir, last_indexed, indexed_files, chunk_count, stale_files, parse_errors, write_errors, index_fresh, freshness_cause, seed_commit? }`.
|
|
691
|
+
|
|
692
|
+
`index_fresh` is `true` only when `stale_files = 0` and the index has been run at least once. `freshness_cause` is `"phase1_stale"` when stale files remain, `null` when fresh. `stale_files` counts changed files, files on disk that are not in the index at all, and indexed files that are gone from disk — the same number `mast status` reports, from the same producer.
|
|
693
|
+
|
|
694
|
+
**Why:** Before a long agentic workflow that depends on accurate code navigation, an assistant can call `mast_status` to confirm the index is fresh, or surface the number of stale files to the user if not.
|
|
695
|
+
|
|
696
|
+
---
|
|
697
|
+
|
|
698
|
+
### `mast_efficiency`
|
|
699
|
+
|
|
700
|
+
Token savings report for the current session or all time.
|
|
701
|
+
|
|
702
|
+
```typescript
|
|
703
|
+
{
|
|
704
|
+
scope: "session" | "global",
|
|
705
|
+
since_minutes?: number // global scope: restrict to last N minutes
|
|
706
|
+
}
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
**Returns:** `{ scope, window_started_at, tokens_returned, tokens_full_file_upper_bound, efficiency_ratio, calls_total, calls_by_tool, tokenizer, counterfactual }`.
|
|
710
|
+
|
|
711
|
+
The `counterfactual` field is a human-readable sentence: *"Would have cost ~14,200 tokens with naive full-file reads; saved ~11,400 tokens (80.3%)."*
|
|
712
|
+
|
|
713
|
+
**Why:** Token efficiency is the whole reason MAST exists, but without measurement it is just a claim. Every tool call records tokens returned to `metrics` asynchronously (fire-and-forget, < 1 ms). `mast_efficiency` aggregates those records so the value of precise code navigation is concrete and auditable.
|
|
714
|
+
|
|
715
|
+
---
|
|
716
|
+
|
|
717
|
+
## Configuration
|
|
718
|
+
|
|
719
|
+
MAST reads configuration from `mast.config.json` in the project root, environment variables, or CLI flags. Priority order (highest to lowest): CLI flag → `MAST_STATE_DIR` env var → `mast.config.json` → built-in defaults.
|
|
720
|
+
|
|
721
|
+
| Key | Default | Description |
|
|
722
|
+
| -------------------------- | --------------------------------------------------------------------------- | ----------------------------------------------------------------------------|
|
|
723
|
+
| `state_dir` | `.mast` | Directory for all index state (relative to project root) |
|
|
724
|
+
| `file_extensions` | `.ts,.tsx,.js,.jsx,.md` | Source file extensions to index |
|
|
725
|
+
| `exclude_patterns` | `node_modules/**`, `dist/**`, `coverage/**`, `.kluster/**`, `**/*.test.ts`, `**/*.spec.ts` | Glob patterns to skip |
|
|
726
|
+
| `rrf_k` | `60` | Reciprocal Rank Fusion constant (higher = flatter ranking) |
|
|
727
|
+
| `declaration_exact_ranker` | `true` | Fuse ranker D (declaration-exact match) into `mast_search`. Set `false` to restore BM25-only ranking without a code change. |
|
|
728
|
+
| `chunk_split_threshold` | `100` | Lines above which a declaration is split into overlapping sub-chunks |
|
|
729
|
+
| `context_lines` | `3` | Source lines before/after AST boundaries included in stored content |
|
|
730
|
+
| `markdown_heading_depth` | `2` | Maximum ATX heading level (`##`) that starts a new markdown doc chunk |
|
|
731
|
+
|
|
732
|
+
**`mast.config.json` example:**
|
|
733
|
+
|
|
734
|
+
```json
|
|
735
|
+
{
|
|
736
|
+
"state_dir": ".mast",
|
|
737
|
+
"exclude_patterns": ["node_modules/**", "dist/**", "**/*.test.ts"],
|
|
738
|
+
"declaration_exact_ranker": true,
|
|
739
|
+
"context_lines": 5
|
|
740
|
+
}
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
**`MAST_STATE_DIR`** — override the state directory without touching `mast.config.json`. Useful in CI or Docker environments where the project root is read-only.
|
|
744
|
+
|
|
745
|
+
---
|
|
746
|
+
|
|
747
|
+
## How It Works
|
|
748
|
+
|
|
749
|
+
### Indexing
|
|
750
|
+
|
|
751
|
+
`runIndex` walks the project with `fast-glob`, computes an mtime-based manifest, and diffs it against the stored manifest to find stale, added, and deleted files. For each file that needs processing:
|
|
752
|
+
|
|
753
|
+
1. **Parse** — `tree-sitter` parses the file into a concrete syntax tree. The TypeScript grammar is used for `.ts` and `.tsx`; the JavaScript grammar for `.js` and `.jsx`. Markdown files are chunked by heading (`markdown_heading_depth`), not parsed with tree-sitter.
|
|
754
|
+
2. **Chunk** — the extractor decomposes the CST into typed chunks: `function`, `class_shell` (the class declaration plus member signatures, without bodies), `method` (individual methods), `interface`, `type`, `export`, `block`, and `doc` (markdown sections). Classes are always decomposed so that a search for a single method doesn't return the entire class body.
|
|
755
|
+
3. **Sub-chunk** — declarations longer than `chunk_split_threshold` lines are split into overlapping segments so no single chunk is too large to be a useful, self-contained search result.
|
|
756
|
+
4. **Symbol graph** — symbols, imports, and edges (IMPLEMENTS, PARENT_OF, POTENTIAL_CALL) are written to SQLite. The two-pass write strategy (all files first, then edges) ensures edges can reference symbols that may be defined in a file parsed later in the same run.
|
|
757
|
+
5. **FTS** — chunk content is written to an FTS5 virtual table with a trigram tokeniser, enabling sub-token and camelCase searches. An `identifier_fts` table with a unicode61 tokeniser handles exact-identifier lookups for `mast_callers` potential matches.
|
|
758
|
+
|
|
759
|
+
Indexing is a single phase — chunk/graph/FTS all update together in one `runIndex` pass; there is no separate embedding step.
|
|
760
|
+
|
|
761
|
+
### Ranked Search (BM25 + Ranker D via RRF)
|
|
762
|
+
|
|
763
|
+
A query goes through two rankers:
|
|
764
|
+
|
|
765
|
+
**BM25 (FTS5):** The query is matched against `chunk_fts` using SQLite's built-in BM25 ranking, over a trigram tokeniser. File-pattern and language filters are pushed into this query as SQL predicates against the `files` table (not as FTS MATCH predicates, because SQLite FTS5 LIKE on UNINDEXED columns is unreliable with MATCH). BM25 scores in SQLite's convention are negative — more negative is a stronger match; `mast_search`'s `match_score` preserves that sign.
|
|
766
|
+
|
|
767
|
+
**Ranker D (declaration-exact):** A direct SQL predicate against `chunks.symbol_name` — full-name match or final-dot-segment match, case-insensitive, deterministically ordered. Gated by the `declaration_exact_ranker` config key (default on).
|
|
768
|
+
|
|
769
|
+
**RRF fusion:** The two ranked lists are combined using Reciprocal Rank Fusion:
|
|
770
|
+
|
|
771
|
+
```
|
|
772
|
+
score(chunk) = Σ 1 / (k + rank(chunk))
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
with default `k = 60`. A chunk appearing at rank 1 in both lists scores twice as high as a chunk appearing in only one. Chunks appearing in only one list still score well — neither signal dominates.
|
|
776
|
+
|
|
777
|
+
### JIT Staleness Checks
|
|
778
|
+
|
|
779
|
+
Every read tool (search, exports, signature, callers, dependencies, implementors) calls `jitRefreshFile` before returning results. This function:
|
|
780
|
+
|
|
781
|
+
1. Reads the stored mtime for the file from the `files` table.
|
|
782
|
+
2. Calls `stat()` on the file on disk.
|
|
783
|
+
3. If the disk mtime is newer, acquires the `structure.lock` and re-parses the file immediately.
|
|
784
|
+
|
|
785
|
+
This means an assistant editing a file and immediately querying it will always see the current version, without waiting for a scheduled reindex. (JIT staleness handles files already known to the index; a brand-new file or symbol still needs `mast_reindex` or the next scheduled/watch reindex to be discoverable.)
|
|
786
|
+
|
|
787
|
+
### Startup Ladder
|
|
788
|
+
|
|
789
|
+
`mast serve` starts accepting MCP connections in under 1 second via a four-step ladder:
|
|
790
|
+
|
|
791
|
+
```
|
|
792
|
+
Step 1 Bootstrap state directory; copy Docker seed layer if present;
|
|
793
|
+
best-effort remove orphaned pre-vector-store state < 500ms
|
|
794
|
+
Step 2 Schema version check; open SQLite < 1s
|
|
795
|
+
Step 3 Register all 11 MCP tools; open stdio transport < 500ms
|
|
796
|
+
Step 4 Background incremental reindex async
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
All tools are ready to serve as soon as Step 3 completes — there is no reduced-capability startup window. When a pre-built seed index is available at `/opt/mast-seed`, it is copied to the state directory in Step 1 — the background reindex in Step 4 then only needs to process files changed since the seed was built.
|
|
800
|
+
|
|
801
|
+
### Concurrency Model
|
|
802
|
+
|
|
803
|
+
One advisory lock coordinates concurrent writers:
|
|
804
|
+
|
|
805
|
+
- **`structure.lock`** — held by `runIndex` and JIT re-parses. Prevents two writers from modifying the SQLite graph simultaneously.
|
|
806
|
+
|
|
807
|
+
The lock uses `proper-lockfile` (POSIX advisory locks via a `.lock` marker file). A 10-second stale lock timeout prevents a crashed process from blocking the system indefinitely. Read tools never acquire a write lock — they may see a briefly inconsistent state during a concurrent reindex, and return `file_busy_returning_stale_cache: true` in that case.
|
|
808
|
+
|
|
809
|
+
### Storage Layout
|
|
810
|
+
|
|
811
|
+
```
|
|
812
|
+
.mast/
|
|
813
|
+
graph.db SQLite — symbols, edges, imports, chunks, FTS5 tables, metrics
|
|
814
|
+
file_manifest.json mtime snapshot from the last index run
|
|
815
|
+
index.json schema version, file count, chunk count, last_indexed
|
|
816
|
+
config.json resolved config written at init/serve time
|
|
817
|
+
structure lock marker (proper-lockfile target)
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
---
|
|
821
|
+
|
|
822
|
+
## Token Efficiency
|
|
823
|
+
|
|
824
|
+
Every tool call records its token count to `metrics` asynchronously. The record includes:
|
|
825
|
+
|
|
826
|
+
- `tokens_returned` — actual tokens in the response (Anthropic CL100k tokenizer)
|
|
827
|
+
- `tokens_full_file_upper_bound` — what a naive full-file read would have cost (when calculable)
|
|
828
|
+
- `duration_ms`, `session_id`, and `status`
|
|
829
|
+
|
|
830
|
+
`metrics_daily` rolls these up by `(day, tool_name)` with a running average for duration and running totals for token counts. The rollup upsert uses an incremental average formula to avoid storing all raw rows indefinitely:
|
|
831
|
+
|
|
832
|
+
```sql
|
|
833
|
+
avg_duration_ms = (old_avg * old_n + new_val) / (old_n + 1)
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
Use `mast metrics --since 7d` for a human-readable table, or `mast_efficiency` from within an MCP session for a machine-readable JSON summary with a `counterfactual` narrative.
|
|
837
|
+
|
|
838
|
+
---
|
|
839
|
+
|
|
840
|
+
## History
|
|
841
|
+
|
|
842
|
+
MAST originally fused BM25 with a vector-embedding search leg (LanceDB + a local ONNX embedding model). Measurement did not support keeping it: the vector store was removed 2026-08-06 per the M2 decision (see [ADR 003](adr/003-2026-08-04-vector-store-deletion.md)). The pre-deletion system — including the embedding pipeline and the eval instruments that measured it — is preserved at the git tag `mast-pre-vector-delete` for anyone re-running that evidence.
|