typegraph
Type-aware TypeScript navigation — 14 MCP tools for go-to-definition, find-references, dependency graphs, cycle detection, and impact analys
Open source Open in the app JSON README (API)
About
Type-aware TypeScript navigation — 14 MCP tools for go-to-definition, find-references, dependency graphs, cycle detection, and impact analysis
Details
- Kind
- Plugins
- Topic
- Developer tools
- Publisher
- guyowen
- Origin
- gemini
- Category
- ferramentas
- Version
- 1.0.0
- Stars
- 12
- Forks
- 2
- Last push
- 2026-08-15T04:58:08Z
- Repository state
- ativo
- Language
- TypeScript
- License
- MIT
- Added
- 2026-08-30 14:13:39
- Updated
- 2026-08-30 14:13:39
- Origin id
guyowen/typegraph-mcp
README
# typegraph-mcp
<p align="center">
<img src="./assets/hero.jpg" alt="typegraph-mcp — Semantic TypeScript understanding for AI agents" width="800">
</p>
Supercharge your AI coding agent with TypeScript 7 semantic understanding.
22 semantic navigation, graph, and TSGo LSP tools delivered via the
[Model Context Protocol](https://modelcontextprotocol.io/) so any MCP-compatible
agent can navigate TypeScript like an IDE instead of grepping through files.
- **Instant type resolution** — hover info, generics, inferred types, and documentation without reading files
- **Instant Effect-aware hover** — expanded `Success`, `Failure`, and `Requirements` blocks when `@effect/tsgo` exposes them
- **Instant call tracing** — follow a symbol from handler to implementation in one call
- **Instant impact analysis** — "what breaks if I change this?" across the entire codebase
- **Instant dependency mapping** — what imports what, direct and transitive, by package
- **Instant Effect diagnostics** — structured LSP rule output and quick fixes for projects using the Effect TSGo toolchain
- **Zero false positives** — semantic references, not string matches
## The problem
AI coding agents navigate TypeScript poorly when they only have text search. They
`grep` for a symbol name and get comments, string literals, tests, aliases, and
barrel exports mixed together. They read whole files to reconstruct a type that
the checker already knows. They guess at blast radius from string matches.
Every wrong turn burns context and makes the next edit less reliable.
## The difference
TypeGraph MCP gives agents narrow semantic tools for the questions they actually
ask while changing code:
| Question | Text search | typegraph-mcp |
|---|---|---|
| Where is this symbol defined? | Reads candidate files and guesses | `ts_definition` resolves through imports, re-exports, barrels, and generics |
| Who uses this? | String matches with false positives | `ts_references` returns semantic references |
| What does this return? | Reads implementations and inferred callers | `ts_type_info` / `ts_hover` ask the checker and LSP |
| What breaks if I change it? | Manual call-chain reconstruction | `ts_blast_radius` and `ts_symbol_overview` summarize usage and package impact |
| What imports this module? | Path-pattern search | `ts_dependents` and `ts_dependency_tree` use the import graph |
| Are Effect rules firing? | Run a separate command and parse output | `ts_effect_diagnostics` returns structured rule diagnostics |
This server drives **tsgo** via `@effect/tsgo` in `--api` mode, with an LSP supplement for editor-style hover, Effect diagnostics/code actions, project-wide local-symbol search, and document symbols. It is built for codebases on TypeScript 7, while still analyzing TypeScript 5 projects through its bundled TSGo compatibility path.
Status: **working end to end.** All 22 MCP tools implemented, verified against a real project on `@effect/tsgo` 0.36.4 / `typescript` 7.0.2. The published package runs compiled `dist/` entrypoints, with no `tsx` runtime dependency. `tsc --noEmit` is clean.
Installed as skills + an MCP entry per agent. Nothing is copied into your project but SKILL.md files — see [Install model](#install-model-skills--mcp-config-nothing-else).
## Why TSGo
TSGo has no `tsserver` entrypoint. It exposes a separate `--api` RPC surface for semantic checker/program operations and an LSP surface for editor-style queries. TypeGraph-Go uses both deliberately:
- `--api` powers definitions, references, raw checker type info, module exports, trace-chain, blast-radius, and the exact export index.
- LSP powers editor-style hover, code actions, optional local-symbol search, and document symbols, including route-table and handler-map keys that project-wide export indexes do not see.
- `@effect/tsgo diagnostics --format json` powers structured Effect Language Service diagnostics.
## Architecture
```
MCP client (Claude Code, OpenCode, Codex, …)
│
├── 7 point-query tools ──► src/api-client.ts
│ typescript/unstable/async
│ └─► effect-tsgo --api (JSON-RPC + msgpack)
│
├── 4 LSP tools ──► src/lsp-client.ts (hover, Layer hover, code actions)
│ └─► effect-tsgo --lsp
│
├── diagnostics ──► @effect/tsgo diagnostics --format json
│
├── ts_navigate_to ──► src/navigate-to.ts (export index + LSP coordinates/locals)
│
├── 4 agent helper tools ──► composites over semantic + graph backends
│
└── 6 graph tools ──► oxc-parser / oxc-resolver (version-independent)
```
The binary comes from `@effect/tsgo` rather than `typescript`, because `--api` survives Effect's patch set untouched (`_patches/typescript-go/001-cmd-tsgo-main.patch` only *adds* a case) while their checker and hover patches make type display Effect-aware. Falls back to typescript's own binary when `@effect/tsgo` isn't installed.
## Tool surface
Point and navigation tools:
- `ts_find_symbol`
- `ts_definition`
- `ts_references`
- `ts_type_info`
- `ts_hover`
- `ts_layer_hover`
- `ts_effect_diagnostics`
- `ts_code_actions`
- `ts_navigate_to`
- `ts_trace_chain`
- `ts_blast_radius`
- `ts_module_exports`
Agent helper tools:
- `ts_project_info` — confirm project root, tsconfig, backend, export-index size, and graph size.
- `ts_document_symbols` — inspect one file's symbols, including object-literal keys.
- `ts_symbol_overview` — one-call definition, type, reference summary, and blast radius for a symbol.
- `ts_dead_exports` — possible unused exports in one module.
Graph tools:
- `ts_dependency_tree`
- `ts_dependents`
- `ts_import_cycles`
- `ts_shortest_path`
- `ts_subgraph`
- `ts_module_boundary`
## Version pinning is load-bearing
`tsgo --api` has **no protocol version handshake**, and its encoders are code-generated. A client/server mismatch surfaces as msgpack decode errors mid-request, not a clean error.
`typescript` is therefore pinned **exact** to `7.0.2` — the version `@effect/tsgo` pins on its `latest` channel (`_packages/tsgo/upstream.json` → gitHead `2bd066d`). `src/version-guard.ts` reads that manifest at runtime and refuses to start on a mismatch.
## `ts_navigate_to`
Benchmarked both viable strategies on a 1506-file fixture (~9000 exports, 3000 non-exported locals):
| | export index (API) | `workspace/symbol` (LSP) |
|---|---|---|
| Coverage | module exports only | every declaration, incl. locals + methods |
| Matching | exact substring | fuzzy subsequence |
| Result cap | none | **256, hard-coded, unflagged** |
| Build cost | ~0.13 ms/file (191ms @ 1506) | none |
| Query | <1ms | 2–6ms |
The index is the default. The 256 cap (`typescript-go` `internal/ls/symbols.go:558`) is silent, and at 1506 files *every* non-trivial query saturated it — which would quietly break the `deep-survey` skill's Phase 3b, where `ts_navigate_to` is used to *count* pattern prevalence.
`includeLocals: true` opts into the LSP path. The result reports `exportHits` and `localHits` separately, with `localsTruncated` scoped to the LSP half — a single blanket `truncated` flag would push callers to discard exact export counts alongside the capped local ones.
`maxResults` (default 10) trims the returned **list**, sorted best-match first, and leaves every count describing the full set. `listTrimmed` therefore means something different from `localsTruncated`: one shortens a list, the other invalidates a count. Collapsing them would put the 256-cap hazard back by another route.
`file` adds one file's document symbols to the search. That is the only route to **object-literal property keys** — RPC handler maps, route tables — which neither backend's project-wide index sees. Measured on 7.0.2 against a handler map:
| | `workspace/symbol "Handler"` | `documentSymbol` |
|---|---|---|
| hits | 1 (the `rpcHandlers` binding) | 10 (every key, incl. nested) |
Two tsgo LSP behaviours worth knowing, both encoded in `src/lsp-client.ts`:
- `workspace/symbol` returns `[]` until a project is loaded — you must `didOpen` a file first.
- The server issues `client/registerCapability` and **blocks** until the client replies.
## TSGo LSP hover, diagnostics, and code actions
The LSP tools are for editor-style semantic feedback, not text search:
- `ts_hover` asks `@effect/tsgo --lsp` for `textDocument/hover`. In Effect projects this can return the richer Effect Language Service presentation, including expanded `Success`, `Failure`, and `Requirements` blocks.
- `ts_layer_hover` is a focused wrapper over the same hover response. It flags Layer hovers and extracts Mermaid graph links when `@effect/tsgo` includes them.
- `ts_effect_diagnostics` runs `@effect/tsgo diagnostics --format json` against either the configured project or one file. It returns structured rule names, codes, severities, ranges, messages, and summary counts. On plain TypeScript TSGo projects, it returns `unavailable: true` with a reason instead of pretending Effect diagnostics ran.
- `ts_code_actions` asks `textDocument/codeAction` for quick fixes and refactors. Diagnostic quick fixes need diagnostics in the request context; refactors can be listed from a selected symbol/range without diagnostics.
Use these before grepping TypeScript when the question is about a symbol's editor hover, Effect channels, Effect diagnostics, available quick fixes, or Layer composition. Use `rg`/`grep` for docs/config/non-TypeScript assets and broad syntactic discovery where no symbol identity is involved.
## Install model: skills + MCP config, nothing else
`setup` writes two kinds of thing into a project — SKILL.md files, and an MCP
server entry in each agent's own config. It does not copy this package
anywhere. There is no plugin directory, no second `node_modules` to install,
and no vendored copy to drift out of sync with the one npm manages.
| Provider | Skills directory | MCP config |
|---|---|---|
| Claude Code | `.claude/skills/` | `.mcp.json` |
| Cursor | `.cursor/skills/`, or rides along with `.claude`/`.agents` | `.cursor/mcp.json` |
| OpenCode | `.claude/skills/` or `.agents/skills/` (whichever is already written) | `opencode.json(c)` |
| Codex | `.agents/skills/` | `.codex/config.toml` |
| Copilot | `.agents/skills/` | `.vscode/mcp.json` |
| Antigravity | `.agents/skills/` | `.agents/mcp_config.json` |
| Gemini CLI | `.agents/skills/` | — none; register manually |
Cursor and OpenCode each read several of these locations, so
`computeSkillTargets()` computes a minimal covering set rather than writing
per-agent. Selecting all seven providers writes **two** directories, not four —
otherwise the flexible agents would discover every skill more than once.
### Which copy of the server gets registered
`resolveServerTarget()` prefers a copy resolvable *from the project* — a real
dependency — and writes a **project-relative**, forward-slash path for it. All
supported MCP configs are project-scoped and normally get committed: an
installer-machine absolute path or Windows-only separator would fail on a
teammate's checkout.
Falling back to the running copy is fine for a global install or a dev
checkout, but an npx-cache path is flagged: npm garbage-collects that
directory, which would leave the entry pointing at nothing.
The literal `node_modules/typegraph-mcp` is preferred over what
`require.resolve` returns, because that is a realpath — under pnpm it is
`node_modules/.pnpm/typegraph-mcp@<version>/node_modules/typegraph-mcp`, a
version-pinned path that dies on the next upgrade. The symlink is the stable
name. Copied skills follow the same rule: from the project root, their health
check calls the package's project-relative public CLI with explicit
`--project-root` and `--tsconfig` options. Both `--flag value` and
`--flag=value` forms are accepted, and duplicates are rejected. Setup
normalizes an in-project absolute tsconfig to a project-relative path, accepts
portable relative parent paths used by monorepos, and rejects an absolute path
outside the project rather than baking an installer-machine location into
committed files. Existing directory paths are rejected as invalid tsconfigs.
Moving the checkout does not stale the command, and the command assumes neither
Git nor POSIX environment syntax.
### Public entry points, no flag
```
typegraph-mcp setup|remove|check the installer
typegraph-mcp serves when stdio is piped; prints usage at a TTY
typegraph compatibility alias for typegraph-mcp
typegraph-mcp-server direct stdio server
```
An MCP entry never has to name a subcommand or a path inside the package —
`npx typegraph-mcp` with stdio piped is a complete server command. The installer
still writes `node <path>` instead, which skips npx's resolver on every server
start and cannot reach the network for a version you did not ask for; both work.
`typegraph-mcp` with no arguments decides by looking at stdin. A client always
arrives with stdio piped; a person at a terminal does not, and gets usage rather
than a process that appears to hang. The public bin is a tiny `dist/cli.cjs`
trampoline in the published package (`src/cli.cjs` in source checkout mode)
that validates Node before importing the implementation; `src/cli.ts`
therefore imports nothing but `node:path` at the top level and loads each branch
dynamically — @clack/prompts writes to stdout, and stdout carries nothing but
JSON-RPC once a client is connected. `tests/cli-dispatch-test.ts` asserts that
statically.
## Installer invariants
1. **`${CLAUDE_PLUGIN_ROOT}` was never expanded** for non-Claude agents. Only Claude Code expands it, and only for plugin-discovered skills — so `.agents/skills/` copies shipped it literally. With no plugin directory at all, nothing would expand it anywhere. Installed project dependencies now use their project-relative public CLI; an external package checkout is the explicitly warned, absolute fallback.
2. **Baked interpreter paths rot.** Every supported config is project-scoped. Committed configs and skills therefore use `node` from PATH instead of an installer-machine nvm/fnm/mise path; the CLI trampoline reports a clear error below Node 22.18, and `typegraph-mcp check` verifies the command actually stored in each config.
3. **Legacy global Codex entries were never removed.** The retired plugin installer wrote a project-specific server into `~/.codex/config.toml`. Current releases use project `.codex/config.toml`, while setup/remove still clean an old global entry when it points at this project.
## Commands
Use Node 24 for development. The repo carries both `.nvmrc` and `.node-version`
with `24.11.0`; `engines.node` remains `>=22.18` because 22.18 is the runtime
floor for native TypeScript type stripping. This package uses npm, so there is
no pnpm-specific Node-version control involved.
```bash
npm test # routing, installer, and live-invalidation tests
npm run check # health check: binary, version skew, stale interpreters
npm run typecheck # tsc --noEmit (TypeScript 7)
# end-to-end against a real project
node tests/server-smoke.ts <projectRoot>
node tests/effect-lsp-smoke.ts <effectProjectRoot> [tsconfig]
node tests/refresh-bench.ts <projectRoot>
```
## Release/versioning
Package versions are bumped in the repo, not by CI. Before pushing a release to
`main`, update `package.json` and `package-lock.json` with the intended version
(`npm version patch --no-git-tag-version`, or the equivalent minor/major bump).
The GitHub Actions publish job validates, builds, publishes exactly the committed
version, creates the matching `vX.Y.Z` tag, and syncs `dev` to `main`.
Verified on a 1506-file fixture against `@effect/tsgo` 0.32.1 / `typescript` 7.0.2:
```
boot + snapshot: 74.9ms binary: @effect/tsgo
index build: 177.3ms -> 31502 entries, 10502 unique (0.118 ms/file)
checker: makeWidget0 : (id: string) => Widget0
```
The 3× gap between raw entries and unique symbols is barrel re-export duplication, collapsed exactly by deduping on `symbol.id`.
## Install
```bash
npm install --save-dev typegraph-mcp # so configs can be relative, and committed
npx typegraph-mcp setup # detects agents, installs MCP + skills
npx typegraph-mcp check # verify
npx typegraph-mcp remove # full round-trip undo
```
`setup` works without the first line — it falls back to absolute package paths
and says so. Installing the dependency first is what makes committed MCP and
skill paths portable across machines and checkout moves.
`check` verifies the three things that otherwise fail silently: the tsgo binary
resolves, the client/binary versions still agree, and every installed config's
PATH-resolved Node is present/new enough while the server path still exists on
disk.
`opencode.jsonc` is handled properly — `src/jsonc.ts` is a string-aware comment
stripper, so a config containing `https://example.com//docs` survives
registration intact. The inherited installer would have hit `JSON.parse`, warned,
and silently skipped registering the server.
### TypeScript runtime and tsconfig
The semantic backend always speaks the TypeScript 7 `--api` protocol. It first
tries the Effect-patched `@effect/tsgo` binary for projects that already carry a
native TypeScript 7 package (`typescript@>=7` or `@typescript/native`). If the
target project is still on TypeScript 5.x, it falls back to this package's
bundled `typescript@7.0.2` binary and analyzes the project in compatibility
mode. That lets TypeScript 5 projects use the tool without changing their own
compiler dependency, while keeping the MCP client and TSGo binary on the same
wire protocol.
TSGo also needs an explicit tsconfig project. The installer writes
`TYPEGRAPH_TSCONFIG=./tsconfig.json` by default; set `TYPEGRAPH_TSCONFIG` before
running setup if the project uses a different root config.
## Build and runtime model
There is no bundler and no `tsx`. Source checkout commands use `.cjs`
trampolines (`src/cli.cjs`, `src/server.cjs`, `src/check.cjs`) that validate
Node before importing the `.ts` implementations, so old Node prints a clear
version error instead of dying on `ERR_UNKNOWN_FILE_EXTENSION`.
The npm package publishes compiled `dist/` entrypoints. That is required because
Node's native TypeScript type stripping deliberately refuses to strip `.ts`
files under `node_modules`; a devDependency install must run `.js`, even on
Node 24.
The source-mode cost is staying inside **erasable syntax**: no parameter
properties, no enums, no namespaces, and `.ts` import specifiers throughout
(strip-only mode does not remap `.js` → `.ts`). The published `dist/` build
rewrites relative `.ts` imports to `.js` before packing.
This avoids an install-time failure mode: `tsx` is not needed at runtime, and
the package is designed for installs that omit dev dependencies.
## Live invalidation
`fs.watch` feeds a dirty set that is applied lazily before each query, so a
burst of edits collapses into one refresh. Measured on the 1506-file fixture
(`tests/refresh-bench.ts`):
| | |
|---|---|
| full re-snapshot | 3.5ms |
| `applyChanges({changed:[1]})` | 1.3ms |
| `reindex(1 file)` | 1.4ms |
| full index rebuild | 115.7ms |
The snapshot was never the expensive part — tsgo keeps the program warm. The
cost is the export index, at ~83× a targeted re-index. That is why `Invalidator`
tracks *which* files changed rather than a dirty boolean: `updateSnapshot`
reports back per-project `changedFiles`, so re-indexing is surgical rather than
inferred.
## Not yet done
- Gemini CLI has no MCP registration path (`mcp: { kind: "none" }`)
- The oxc module graph has its own `startWatcher`, currently unused by the server — graph tools rebuild per session
- `fs.watch({recursive:true})` is fine on macOS/Windows and Linux ≥20, but unwatched failures degrade silently to "snapshot as of last explicit refresh"