Skip to main content
Glama
README.md
# codegraph πŸ•Έ β€” Your codebase as a graph, for your eyes and your agent

<p align="center">
  <img src="docs/screenshot.png" alt="codegraph viewer showing a 400-file Next.js app as 1,854 nodes and 4,625 edges">
</p>

<p align="center">
  <a href="https://github.com/RexHung0302/codegraph/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/RexHung0302/codegraph/ci.yml?branch=main&style=flat-square&label=ci" alt="CI status"></a>
  <a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%3E%3D20-brightgreen?style=flat-square" alt="Node >= 20"></a>
  <a href="https://modelcontextprotocol.io"><img src="https://img.shields.io/badge/MCP-server-7c5cff?style=flat-square" alt="MCP server"></a>
  <img src="https://img.shields.io/github/languages/top/RexHung0302/codegraph?style=flat-square" alt="Top language">
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="License: MIT"></a>
  <a href="#development"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen?style=flat-square" alt="PRs welcome"></a>
</p>

codegraph parses a TypeScript / JavaScript / Vue repository into a graph of files, symbols, and the edges between them β€” imports, calls, component usage, inheritance. A WebGL viewer makes the structure visible; an MCP server lets a coding agent query it instead of grepping.

[Quick start](#quick-start) Β· [Type-aware mode](#type-aware-mode) Β· [Monorepos](#monorepos) Β· [MCP tools](docs/mcp-tools.md) Β· [Recipes](docs/recipes.md) Β· [Benchmarks](docs/benchmarks.md) Β· [Graph schema](docs/graph-schema.md) Β· [繁體中文](README.zh-TW.md)

## Why

`grep` answers "where does this string appear". It does not answer "what breaks if I change this", "which files are the risky hubs", or "what shape is this codebase". codegraph extracts the structure once, and both you and your agent query it from then on.

On a 400-file app, asking which files matter costs 369 bytes through `hubs` versus 29.7 KB of file reading β€” roughly 80Γ— less context for the same answer. [Full numbers](docs/benchmarks.md).

## Install

Node >= 20 and pnpm (npm and yarn work too). Nothing is published to npm yet β€” clone and run it.

```bash
git clone https://github.com/RexHung0302/codegraph.git
cd codegraph
pnpm install
```

## Quick start

```bash
# 1. index a repository
pnpm extract /path/to/your/repo

# 2. look at it
pnpm dev            # http://localhost:5173

# 3. or let an agent query it
pnpm mcp
```

```
pnpm extract <repo-path> [--out <file>] [--exclude dir1,dir2] [--types]
```

| Flag | Default | Meaning |
|---|---|---|
| `--out` | `web/public/graph.json` | where to write the graph |
| `--exclude` | – | extra directory names to skip, comma separated |
| `--types` | off | resolve through the TypeScript checker instead of by name β€” see [Type-aware mode](#type-aware-mode) |

Always skipped: `node_modules`, `.git`, `.next`, `dist`, `build`, `out`, `coverage`, `.turbo`, `.vercel`, `.output`, `storybook-static`, and every `.d.ts`.

> Exclude generated code. Indexing a Prisma client adds thousands of `Type` nodes that drown out everything else β€” `--exclude generated` is a common first move.

## How it fits together

Three independent pieces, one JSON file between them.

- **`src/extract.ts`** walks the AST with [ts-morph](https://ts-morph.com) and writes `graph.json`. The only piece that knows about source languages.
- **`web/`** renders the graph with [sigma.js](https://www.sigmajs.org) and graphology, ForceAtlas2 running in a worker.
- **`src/mcp.ts`** serves the same file over [MCP](https://modelcontextprotocol.io) β€” seven tools, hot-reloading when the graph changes on disk.

| Node | Extracted from | | Edge | Meaning |
|---|---|---|---|---|
| `Folder` / `File` | directory tree | | `contains` | folder β†’ folder / file |
| `Function` | declarations and arrow-function bindings | | `defines` | file β†’ symbol it declares |
| `Class` / `Interface` / `Type` / `Enum` | corresponding declarations | | `imports` | file β†’ file |
| `Variable` | remaining top-level bindings | | `calls` | caller β†’ callee |
| | | | `renders` | file β†’ component it uses |
| | | | `inherits` | class `extends` / `implements` |

## Type-aware mode

By default every edge is resolved by *name*, which is fast and wrong in the corners: a re-export has no declaration to bind to, and `repo.name()` cannot be told apart from an imported function called `name`.

`pnpm extract <repo> --types` runs the same walk with the TypeScript checker turned on. Each identifier is resolved through its symbol first, and the name-based path stays as the fallback, so the mode only ever *adds* edges β€” it never drops one the default run found.

| | name-based (default) | `--types` |
|---|---|---|
| Re-exports (`export { x } from './x'`) | missed | followed to the real declaration |
| Method call on a typed receiver | attributed to any same-named symbol in scope, or missed | attributed to the method's class |
| Same-name collisions | can produce a wrong edge | resolved by the checker |
| Vue SFC templates, `.vue` imports | name-based | unchanged β€” the checker cannot resolve `.vue` modules |
| Measured cost (400-file Next.js app) | 1.3 s, 246 MB | 5.2 s, 706 MB |
| Measured gain (same app) | – | +168 edges (+140 `calls`, +28 `renders`), 0 removed |

Memory is the real cost: the default run frees each file's AST as it goes, and the checker cannot, because a file parsed later resolves symbols back into it. On a repo big enough to matter, run it once with `--types` for a reference graph and stay on the default for quick re-extractions.

`meta.typeAware` in `graph.json` records which mode produced it; `graph_stats` reports it, and `reindex` keeps it unless you pass `types`.

## Monorepos

**Point the extractor at one package, not at the workspace root.** Only the configs at the repo root are read β€” `tsconfig.json`, `jsconfig.json`, referenced project configs, and the bundler config next to them β€” so from the root of a pnpm/turbo workspace the per-package `paths` aliases are invisible and imports come out near zero.

```bash
pnpm extract apps/web        --out graphs/web.json
pnpm extract packages/ui     --out graphs/ui.json
pnpm mcp graphs/web.json     # one graph per MCP server
```

Consequences worth knowing before you trust the output:

- **Cross-package edges do not exist.** `import { Button } from '@acme/ui'` resolves to a `node_modules` symlink, which is never indexed β€” so a package's graph shows it as having no outside dependents. Impact analysis is per package.
- **A package's own aliases work** as long as its `tsconfig.json` sits at the path you extracted, which is the normal layout.
- **Extracting the workspace root "works"** in that it produces a graph β€” files and folders are all there β€” but `imports`, `calls` and `renders` collapse. Measured on one pnpm workspace: the root gives 1,610 files and 646 `imports`; its largest app alone gives 601 files and **1,253** `imports`. Fewer edges than files on a monorepo is this limitation, not a sparse codebase.

Workspace-wide extraction (reading every package's config and linking `@scope/pkg` specifiers to the package that declares them) is on the roadmap, not implemented.

## MCP server

```bash
claude mcp add codegraph -- pnpm --dir /path/to/codegraph mcp /path/to/graph.json
```

| Tool | Answers |
|---|---|
| `graph_stats` | which repo is loaded, counts by type |
| `search_nodes` | name β†’ node ids |
| `node_info` | direct neighbours of a node |
| `impact` | transitive dependents / dependencies |
| `hubs` | most-connected, riskiest-to-change nodes |
| `orphans` | files nobody imports (dead-code candidates) |
| `reindex` | re-parse the repo and reload, without restarting |

The server reloads whenever the graph file changes on disk, so `pnpm extract` in another terminal takes effect on the next tool call. Restarting is never required.

## Documentation

| Goal | Start here |
|---|---|
| Use it day to day | [Recipes](docs/recipes.md) β€” seven concrete workflows |
| Judge whether it is worth it | [Benchmarks and cost](docs/benchmarks.md) β€” measured context savings |
| Drive it from an agent | [MCP tools](docs/mcp-tools.md) β€” parameters and sample output |
| Build something on the graph | [Graph schema](docs/graph-schema.md) β€” node ids, edge semantics, guarantees |
| Contribute | [CLAUDE.md](CLAUDE.md) β€” layout, conventions, and how to verify a change |

## Limitations

Read these before trusting an edge.

- **No type checker unless you ask for one.** By default call edges resolve by *name*: a declaration in the same file, otherwise a name imported into it. Dynamic dispatch, re-exports, and method calls on typed receivers are missed; identically-named symbols can produce a wrong edge. [`--types`](#type-aware-mode) fixes the last three; dynamic dispatch stays out of reach.
- **Vue templates are not parsed.** `renders` edges for SFCs come from one SFC importing another, which works with html, pug, and jsx templates alike β€” but globally registered components are invisible.
- **Monorepos are not handled.** Only the configs at the repo root are read, and cross-package imports resolve into `node_modules`, which is never indexed. Extract one package at a time β€” [details](#monorepos).
- **No incremental updates.** Every run is a full re-parse.
- **TS / JS / Vue only.** Svelte, Python, Go and friends are not supported.
- **The viewer loads the whole JSON.** Tens of thousands of nodes render, but ForceAtlas2 needs time to settle and the result is a hairball without clustering.

## Development

```bash
pnpm test            # vitest: 52 cases over React, Vue and type-checker fixtures
pnpm exec tsc --noEmit
pnpm build           # production build of the viewer
```

Tests run the extractor against the miniature repos in `tests/fixtures/` and drive the MCP server over a real stdio transport. CI runs both on Node 20 and 22. When you change extraction, add a fixture case rather than adjusting an assertion to match the new output.

Roadmap: incremental extraction, clustering in the viewer, Svelte and Astro support, and workspace-wide extraction for monorepos.

## License

MIT β€” see [LICENSE](LICENSE).