arcscope
v0.2.0
Published
Fully-local, architecture-aware code-navigation MCP server. tree-sitter breadth + a repo-declared architecture vocabulary answered live.
Maintainers
Readme
arcscope
A fully-local MCP server that gives an AI coding agent precise views of an unfamiliar codebase — fast symbol navigation, a compiler-exact call graph and flow surface, and a repo-declared architecture vocabulary answered live against current code — so it stops re-deriving structure with grep, and stops missing the this.service.foo() calls grep can't see.
Status: 0.1.0. Three tiers — tree-sitter breadth, an agent-authored knowledge layer, and a compiler-exact TypeScript precise tier — work end-to-end. TS / JS / TSX.
npm i -D arcscope
npx arcscope init # index once, write MCP config, update .gitignore
# reload Cursor (Settings → MCP) or restart Claude Codeinit writes the same offline node … serve entry to .cursor/mcp.json (Cursor) and .mcp.json (Claude Code). When arcscope is installed under node_modules, the path is project-relative so you can commit .cursor/mcp.json for the team.
Everything runs on your machine: no network at query time or at server spawn, no telemetry, no embeddings. Breadth comes from tree-sitter (WASM grammars bundled offline); precision comes from the TypeScript compiler running locally. Every result carries a precision tier so the agent never mistakes a heuristic for a compiler-accurate answer.
How it's being used
npx arcscope statsOne local command — no network, no telemetry — that shows both halves:
How much — reads
.arcscope/usage.jsonl(one line per tool call, written as the server runs): totals, a breakdown by tool, and the most-queried symbols/concepts/entry points, plus your committed concepts and their drift baselines.Is it useful (vs grep) — the server only sees its own calls, so
statsalso reads the newest Claude Code session transcript for this repo, replays every search/navigation call in order (arcscope ✅ vs grep 🔎 vs ToolSearch 🔍), and reports arcscope's share ofarcscope + grep. No transcript yet → it says so and shows the usage half only.Reads Claude Code transcripts only (
~/.claude/projects); Cursor sessions aren't included. The share is a whole-session volume ratio — a quick "is the agent reaching for it?" gut check, not a formal adoption gate.
Tools
| tool | answers |
| --- | --- |
| find_def | where a symbol is defined, with its signature |
| find_refs | who references it — compiler-exact for methods when a tsconfig governs the file (member access obj.method() resolved); import-resolved otherwise |
| dep_graph | the file/module dependency graph: hubs, a file's neighborhood, or circular dependencies |
| call_graph | the method-resolved outgoing call graph from a function — follows this.x.foo() through the type checker |
| flow | the complete surface of a flow before you change it: the call closure + a structural edge-case checklist |
| arch_list | the repo's named architecture concepts (committed + agent-asserted) |
| arch_query | resolve one concept to its live code locations, with drift + conformance |
| arch_assert | record a concept — or a reviewed flow — so a later session inherits it, re-verified live |
| arch_candidates | find a concept's likely missing members by structural (AST-shape) similarity |
The precise tier — compiler-exact, still local
tree-sitter can't tell you who calls this.documentService.clone() — resolving a method call needs the receiver's type. arcscope's precise tier runs the TypeScript compiler + language service locally (no network) to answer that exactly:
find_refson a method finds its member-access call sites (obj.method()), resolved through the type checker — the call sites grep and import-resolution miss.call_graphtraces the outgoing closure from an entry point with method dispatch resolved to the concrete implementation (DI-injected services, inheritance).flowmaps the complete surface of a flow before you touch it: every function it transitively calls, annotated with each function's edge cases (branches, error handling, async). So you catch every case a change must handle.
flow cloneUser
# Flow surface from `cloneUser` (apps/.../clone-user.service.ts:44) — 38 functions, precision tier: typescript:
# cloneUser apps/.../clone-user.service.ts:44 (+5 lib) {2 branch}
# ├─ loadRecord apps/.../record.repository.ts:183 (+2 lib) {2 branch, 3 await}
# │ └─ …
# └─ performClone apps/.../clone-user.service.ts:62 (+3 lib) {1 err, 1 await}
# └─ cloneRecord libs/.../record-clone.ts:32 (+1 lib)
# └─ cloneItem libs/.../record-clone.ts:104
# └─ reindexAfterClone libs/.../reindex.ts:41
# … (38 functions; tree abbreviated)
# Edge-case surface: 6 decision points (if/switch/ternary) · 1 error-handling site (try/throw) · 7 async boundaries (await).
# Each {…} tag marks where behaviour forks, fails, or awaits — verify your change handles each before you write it.The architecture vocabulary — the differentiator
Where other tools keep project knowledge as static prose that silently rots, arcscope binds each named concept to an executable locator recomputed on every query. The knowledge is agent-authored, not hand-edited: as the agent works out a concept, it records it with arch_assert into a committed .arcscope/assertions.yaml — a binding of locators, never a frozen list — so a later session inherits it. One concept looks like:
concepts:
repository-tokens:
title: Repository-token pattern (I{Name}Repository)
locators:
- { kind: symbol, query: "interface I*Repository", in: "libs/data-access/**" }
- { kind: symbol, query: "const *_REPOSITORY = InjectionToken", in: "libs/**/tokens/**" }
- { kind: path, glob: "apps/**/firestore-*.repository.ts" }arch_query repository-tokens resolves this against the current tree and flags drift when the resolved set diverges from its accepted baseline. Locators come in three kinds — symbol (a tree-sitter query), path (a glob), and import (every file importing a module specifier) — and resolve through arcscope's own engine; a committed assertion can never run a shell command.
Three things keep an agent-written assertion honest:
- a
mustinvariant — a rule every member must satisfy, re-checked live (conformance); a violation surfaces on every query, not when it ships. arch_candidates— finds the members a binding missed, by AST-shape similarity (name-independent, so it catches a hand-copied re-implementation that shares no name or import). The "fixed it twice" antidote.- a flow concept —
arch_asserta reviewed flow by its entry point;arch_queryrecomputes the whole flow live (precise tier) and drifts when a function enters or leaves it.
Nothing is stored as a bare fact: every assertion is re-verified against live code on read, so it can't silently rot.
Drift in practice
The first arch_query records a baseline; later queries compare against it:
arch_query repository-tokens
# Concept `repository-tokens` — Repository-token pattern (7 locations, fresh (baseline captured)):
# … later, someone adds apps/audit/firestore-audit.repository.ts …
arch_query repository-tokens
# Concept `repository-tokens` — Repository-token pattern (8 locations, DRIFTED):
# ⚠ DRIFT vs baseline: 1 added, 0 removed, 0 changed.
# + apps/audit/firestore-audit.repository.ts
arch_query repository-tokens reaccept:true # accept the new shape → fresh againHow it works
One local Node process speaking MCP over stdio, in tiers:
- Engine (tree-sitter) — web-tree-sitter parses each file (lazy WASM grammars) into symbols + import edges. The always-on breadth substrate.
- Graph — a derived view over those import edges (dependency graph, cycles), grouped by directory + import-clustering — never a build tool's project model (
nx.json, BUILD files). - Knowledge — the live, agent-authored architecture vocabulary above.
- Precise tier (TypeScript) — a local
ts.Program+ language service, built lazily pertsconfigand cached, powering the call graph, flow surface, and compiler-exact method references.
Precision tiers & limits
Two tiers, labeled on every result:
tree-sitter— fast, broad (multi-language breadth), structural. The always-on substrate; complete for imported-symbol references.typescript— compiler-exact, via the local TypeScript compiler + language service. Resolves method dispatch, DI, inheritance, overloads.
arcscope never presents a tree-sitter heuristic as compiler-exact. Honest residual limits (the precise tier counts these at the boundary, never hides them):
any-typed / higher-order callbacks and runtime-wired providers (InjectionToken,useFactory) may resolve to the declared member rather than the concrete impl.- the precise tier is per-
tsconfig— a single flow rarely spans module systems, but cross-project closures aren't unioned. It builds a TS program on first use (seconds), cached after. - TS / JS / TSX only; module grouping is general-first (directory + import clustering), never a build tool's project model.
