project-map-mcp
v0.1.0
Published
MCP server giving coding agents persistent, queryable architectural memory of a codebase.
Maintainers
Readme
project-map-mcp
An MCP (Model Context Protocol) server that gives a coding agent persistent, queryable architectural memory of a codebase.
The map is a graph of small linked YAML files stored in your repo under
.projectmap/. The server reads and updates it — it never invents it from
scratch on each run.
This is a memory/context product, not a validation product. Change-checking is advisory signal for a human or agent to act on, never an automated hard gate.
How it works
- Read the relevant slice of the map before working on something (
get_context). - Check a diff against declared boundaries and invariants after acting (
check_changes). - Propose updates to the map when a change is architectural, so the map
stays current instead of rotting (
propose_map_update).
Install
npm install
npm run buildThe map on disk
.projectmap/
index.yaml # node registry + edge list (the graph spine)
modules/
billing.yaml # one file per module node
auth.yaml
ledger.yaml
invariants/
payments.yaml # reusable invariant sets, referenced by nodes
workflows/
checkout.yaml # optional: key end-to-end workflows
symbols/
billing.yaml # generated by describe_symbols — never hand-editBootstrap one from an existing codebase with:
npx project-map init --root /path/to/repoThis scans top-level source directories, fills in owned_paths automatically,
and derives depends_on edges from the import graph. responsibility,
invariants, and sensitive are left as TODO placeholders — write those by
hand; they're the semantic layer no tool can infer.
Re-run drift detection any time with:
npx project-map sync --root /path/to/repoRunning the MCP server
PROJECTMAP_ROOT=/path/to/repo npx project-map-mcp(or pass the repo root as the first CLI argument; defaults to process.cwd()).
Registers five tools over stdio: get_context, check_changes, propose_map_update,
impact_of_change, describe_symbols.
check_changes's optional LLM summary and describe_symbols's purpose generation use the
Anthropic API — set ANTHROPIC_API_KEY, or call check_changes with includeLlmSummary: false.
describe_symbols only needs a key if some of your exported symbols have no JSDoc; JSDoc'd
symbols never touch the API.
impact_of_change — the blast radius of a change
get_context answers "what should I know before editing this" with a 1-hop,
bidirectional neighbourhood. impact_of_change answers a different question —
"what breaks if I change this" — by walking depends_on edges transitively,
in the direction of dependents only, as far as the graph goes (or maxDepth
hops, if you cap it). Each impacted node comes back with its shortest
dependency chain back to the target and its sensitive flag, so a widely
depended-on, sensitive node stands out immediately. It also flags any
depends_on cycles touching the impact set — those are a real architectural
smell (the dependency graph is supposed to be a DAG) that's easy to miss by
reading code but falls straight out of this traversal.
Call it before touching a node other things depend on, not after — that's
the point: know the blast radius before you make the change, not once
check_changes tells you what broke.
describe_symbols — the function/method dictionary
The map alone tells an agent which module owns something, not what's callable in it — so
get_context still meant grepping the actual files to find out. describe_symbols closes that
gap: it extracts every exported function/class/method in a module (name, signature, file:line)
via a deterministic TypeScript AST parse — no type-checker, no tsconfig resolution, so it's
robust against any target repo — and attaches a one-line purpose to each one.
That purpose comes from two places, in order:
- Existing JSDoc, read straight from the source. Free, instant, no API call.
- An LLM call, only for exported symbols that have no JSDoc.
Both structural facts (signature, location) and purpose are cached in
.projectmap/symbols/<node>.yaml by content hash — a symbol's hash changes if its signature
or body changes, so re-running describe_symbols after edits only re-describes what actually
changed (or newly lacks JSDoc), never a full rescan-and-redescribe. This is a generated file;
don't hand-edit it.
get_context always shows a module's symbol surface (includeSymbols: true by default) —
signatures live, straight from the AST, every call; purposes from whatever's cached. It never
triggers an LLM call itself. describe_symbols is the one tool in this package that costs API
calls, so it's a separate, deliberate step — run it after a batch of changes, not as a side
effect of every get_context.
CLI
project-map init # scaffold .projectmap/ from repo structure
project-map sync # report drift, write nothing
project-map check [--strict] # run check_changes against staged git changes
project-map impact <node-id> [--max-depth N]
# print the blast radius of changing a node
project-map describe-symbols [<node-id>]
# build/refresh the exported-symbol dictionary
project-map install-hook [--pre-commit]
# install the advisory git hook (default: pre-push)--strict exits non-zero when check_changes reports a severity: error
finding. Without it, the CLI (and the installed git hook) is always advisory:
it prints findings and exits 0.
Core design principles
- The graph is dumb; the tools are smart. Nodes, edges, and path ownership only. All intelligence lives in the tools that read the graph.
- Partial retrieval is the whole point. Agents load a subgraph (a node plus its 1-hop neighbours), never the entire map.
- Structural facts are auto-derived; semantic facts are hand-written. Never ask an LLM to infer what can be read deterministically from the codebase.
- Deterministic first, LLM second. Boundary/invariant checks are deterministic graph traversal. The LLM only summarises semantic change for humans, and its output is advisory — it never sets the verdict and never invents invariants not present in the map.
Non-goals / guardrails
check_changes'sverdictgates only on deterministicseverity: errorfindings, and only under--strict. The tool never blocks work by default.- The LLM never sets the verdict and never invents invariants.
- The map is never auto-derived wholesale by the LLM at runtime; structural
sync (
init/sync) is deterministic, semantic content is human-authored. init/sync's import-graph derivation targets TypeScript/JavaScript repos. Other languages can still use the map (paths + hand-written semantics); only that specific derivation is JS/TS-specific.- No VS Code extension in v1 — the logic lives in
src/coreandsrc/tools; a future extension can wrap it. describe_symbolsnever uses the LLM for signatures, names, or locations — those come from a deterministic AST parse and are always ground truth. The LLM only ever supplies a one-line purpose, and only for symbols with no JSDoc; that purpose is advisory, same ascheck_changes's summary.
Repo layout
src/
schema/ # zod defs + types for nodes, edges, the graph, the symbol cache
core/ # loadGraph, resolveFiles, neighborhood, invariantsFor, getDiff,
# impact (blast radius + cycle detection), symbols (AST extraction),
# symbolCache (hash-diff + persistence), ...
tools/ # get_context, check_changes, propose_map_update, impact_of_change,
# describe_symbols
mcp/ # MCP server, stdio transport
llm/ # Anthropic wrappers + prompt templates (advisory summaries/purposes only)
cli.ts
test/
fixtures/.projectmap/ # billing/ledger/auth/checkout-api + payments invariant set
fixtures/src/ # small real .ts files backing the symbol-extraction tests
*.test.tsTesting
npm test