@ashkand/code-graph-mcp 0.2.2
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/README.md +280 -0
- package/bin/code-graph-mcp.js +67 -0
- package/package.json +51 -0
- package/skills/code-graph/SKILL.md +72 -0
- package/src/graph.js +193 -0
- package/src/indexer.js +434 -0
- package/src/install.js +73 -0
- package/src/parsers.js +158 -0
- package/src/server.js +149 -0
- package/src/tools.js +357 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dorkian
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# code-graph-mcp
|
|
2
|
+
|
|
3
|
+
Local code-intelligence MCP server for mono-repos. It indexes your repo once
|
|
4
|
+
into a knowledge graph — packages, components, frontend routes, HTTP calls,
|
|
5
|
+
backend routes, handlers — and lets any MCP client (Claude Code, Cursor, Codex
|
|
6
|
+
CLI, OpenCode…) query structure directly instead of grepping and re-reading
|
|
7
|
+
files on every question.
|
|
8
|
+
|
|
9
|
+
Ask "where does `LoginPage` land in the backend?" and get
|
|
10
|
+
`LoginPage → POST /api/auth/login → auth.login` in one tool call, from a graph
|
|
11
|
+
that's already in memory. No file reads, no grep round-trips.
|
|
12
|
+
|
|
13
|
+
## Why
|
|
14
|
+
|
|
15
|
+
Agents burn most of their tokens re-discovering structure: listing directories,
|
|
16
|
+
grepping for usages, opening five files to trace one request path. This server
|
|
17
|
+
front-loads that discovery into a single index pass and then answers structural
|
|
18
|
+
questions from memory, with output engineered to be cheap:
|
|
19
|
+
|
|
20
|
+
- **Compact line-oriented output** — no pretty-printed JSON, no prose padding.
|
|
21
|
+
- **Hard token budgets per response** (default ~2k tokens, tunable per call)
|
|
22
|
+
with `…+N more (refine query)` truncation instead of overflow.
|
|
23
|
+
- **Path compression** — repo-relative paths, common prefixes stripped in lists.
|
|
24
|
+
- **Incremental re-index** — content-hash manifest; unchanged files are never
|
|
25
|
+
reparsed.
|
|
26
|
+
- **Savings tracking** — `repo_summary` reports how many tokens of raw file
|
|
27
|
+
reads the session avoided.
|
|
28
|
+
- **Zero query-time I/O** — every tool answers from in-memory indexes; only
|
|
29
|
+
`reindex` touches disk.
|
|
30
|
+
|
|
31
|
+
## What it detects
|
|
32
|
+
|
|
33
|
+
| Layer | Detected |
|
|
34
|
+
|---|---|
|
|
35
|
+
| Packages | npm workspaces, any `package.json`, `pyproject.toml`, `requirements.txt` |
|
|
36
|
+
| Frontend | React Router routes (`<Route>`, `createBrowserRouter`), exported components, imports, rendered children, custom hooks/stores |
|
|
37
|
+
| HTTP calls | `fetch()`, `axios.<method>()`, `axios({...})`, generic `client.get/post/...()`, template-literal URLs normalized (`` `${API}/users/${id}` `` → `/users/:param`) |
|
|
38
|
+
| Backend | FastAPI (`@app.get`, `APIRouter(prefix=…)`, `include_router(…, prefix=…)` incl. import aliases), Flask (`@app.route`), Express (`app.get`, `router.post`, `app.use('/prefix', router)`) |
|
|
39
|
+
| Cross-layer | frontend call ↔ backend route matching by method + normalized path, `:param`/`{id}`/`${id}` all treated as wildcards |
|
|
40
|
+
|
|
41
|
+
Parsing is regex/heuristic (deliberately: fast, zero native deps). It won't
|
|
42
|
+
catch every dynamic pattern — that's the tradeoff for indexing thousands of
|
|
43
|
+
files in milliseconds with two pure-JS dependencies.
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
### Fastest: let the CLI configure your client
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# Claude Code — writes .mcp.json AND installs the companion skill
|
|
51
|
+
npx -y @ashkand/code-graph-mcp install claude-code --repo .
|
|
52
|
+
|
|
53
|
+
# Cursor — writes .cursor/mcp.json
|
|
54
|
+
npx -y @ashkand/code-graph-mcp install cursor --repo .
|
|
55
|
+
|
|
56
|
+
# anything else — prints ready-to-paste config for Claude Desktop, Codex CLI, …
|
|
57
|
+
npx -y @ashkand/code-graph-mcp install
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
No npm? Every command also works straight from the repo by swapping
|
|
61
|
+
`@ashkand/code-graph-mcp` for `github:dorkian/code-graph-mcp`.
|
|
62
|
+
|
|
63
|
+
That's it — restart your client and ask *"where does LoginPage land in the backend?"*
|
|
64
|
+
|
|
65
|
+
### Or: let your AI install it
|
|
66
|
+
|
|
67
|
+
Paste this into Claude Code, Cursor, Codex, or any coding agent with shell access, from inside the repo you want indexed:
|
|
68
|
+
|
|
69
|
+
```text
|
|
70
|
+
Install the code-graph-mcp server in this repo:
|
|
71
|
+
1. Run: npx -y @ashkand/code-graph-mcp install claude-code --repo .
|
|
72
|
+
(if this isn't Claude Code, run `npx -y @ashkand/code-graph-mcp install`
|
|
73
|
+
and apply the printed config for the client you are instead)
|
|
74
|
+
2. Add `.codegraph/` to .gitignore if it isn't there.
|
|
75
|
+
3. Run: npx -y @ashkand/code-graph-mcp --repo . --index
|
|
76
|
+
and show me the node/edge counts it reports.
|
|
77
|
+
4. From now on, prefer the code-graph MCP tools (repo_summary,
|
|
78
|
+
find_component_relations, map_frontend_to_backend, search_symbols,
|
|
79
|
+
impact_of_change, visualize_graph) over grep/read_file for questions
|
|
80
|
+
about repo structure, and call reindex after you edit files.
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Step 4 is a summary of the bundled skill — Claude Code users get the full version automatically at `.claude/skills/code-graph/SKILL.md`.
|
|
84
|
+
|
|
85
|
+
### Manual
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
git clone https://github.com/dorkian/code-graph-mcp.git
|
|
89
|
+
cd code-graph-mcp && npm install
|
|
90
|
+
node bin/code-graph-mcp.js --repo /path/to/your/repo
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The graph lives at `<repo>/.codegraph/` — add it to your repo's `.gitignore`.
|
|
94
|
+
Pre-warm the cache (e.g. in CI or a git hook):
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
code-graph-mcp --repo . --index # incremental
|
|
98
|
+
code-graph-mcp --repo . --index --full # rebuild from scratch
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Client setup (manual configs)
|
|
102
|
+
|
|
103
|
+
<details>
|
|
104
|
+
<summary>Claude Code (.mcp.json)</summary>
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"mcpServers": {
|
|
109
|
+
"code-graph": {
|
|
110
|
+
"command": "npx",
|
|
111
|
+
"args": ["-y", "@ashkand/code-graph-mcp", "--repo", "."]
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Then copy `skills/code-graph/` into `.claude/skills/` (the `install claude-code` command does both).
|
|
118
|
+
</details>
|
|
119
|
+
|
|
120
|
+
<details>
|
|
121
|
+
<summary>Cursor (.cursor/mcp.json)</summary>
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"mcpServers": {
|
|
126
|
+
"code-graph": {
|
|
127
|
+
"command": "npx",
|
|
128
|
+
"args": ["-y", "@ashkand/code-graph-mcp", "--repo", "${workspaceFolder}"]
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
</details>
|
|
134
|
+
|
|
135
|
+
<details>
|
|
136
|
+
<summary>Claude Desktop (claude_desktop_config.json)</summary>
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{
|
|
140
|
+
"mcpServers": {
|
|
141
|
+
"code-graph": {
|
|
142
|
+
"command": "npx",
|
|
143
|
+
"args": ["-y", "@ashkand/code-graph-mcp", "--repo", "/abs/path/to/repo"]
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
</details>
|
|
149
|
+
|
|
150
|
+
<details>
|
|
151
|
+
<summary>Codex CLI (~/.codex/config.toml)</summary>
|
|
152
|
+
|
|
153
|
+
```toml
|
|
154
|
+
[mcp_servers.code-graph]
|
|
155
|
+
command = "npx"
|
|
156
|
+
args = ["-y", "@ashkand/code-graph-mcp", "--repo", "/abs/path/to/repo"]
|
|
157
|
+
```
|
|
158
|
+
</details>
|
|
159
|
+
|
|
160
|
+
Any other MCP client: it's a plain stdio server — run
|
|
161
|
+
`node bin/code-graph-mcp.js --repo <path>` and speak MCP over stdin/stdout.
|
|
162
|
+
|
|
163
|
+
## Use it as a Claude custom connector (claude.ai / Desktop / mobile)
|
|
164
|
+
|
|
165
|
+
Claude's custom connectors only accept **remote** MCP servers: Claude connects
|
|
166
|
+
to your server URL from Anthropic's cloud, not from your machine, so a local
|
|
167
|
+
stdio process can't be added directly — the server must be reachable on the
|
|
168
|
+
public internet over Streamable HTTP. That's what `--http` mode is for:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
# on your VPS, next to a checkout of the repo you want indexed
|
|
172
|
+
npx -y @ashkand/code-graph-mcp --repo /srv/my-monorepo \
|
|
173
|
+
--http --port 3333 --auth-token "$(openssl rand -hex 24)"
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Put it behind your reverse proxy with TLS (Caddy example):
|
|
177
|
+
|
|
178
|
+
```caddy
|
|
179
|
+
graph.yourdomain.com {
|
|
180
|
+
reverse_proxy 127.0.0.1:3333
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Then in Claude: **Settings → Connectors → Add custom connector** and paste
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
https://graph.yourdomain.com/mcp/<your-token>
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
The token-in-URL form exists exactly for this: the connector UI takes a plain
|
|
191
|
+
URL (or OAuth), so the secret rides in the path; API/CLI clients can send
|
|
192
|
+
`Authorization: Bearer <token>` instead. Keep in mind what you're exposing —
|
|
193
|
+
the graph reveals your repo's structure (routes, endpoints, file names), so
|
|
194
|
+
treat the URL like a password, rotate the token if it leaks, and never run
|
|
195
|
+
`--http` without a token on anything internet-facing. Run it as a systemd
|
|
196
|
+
service and add a cron/git-hook `--index` call to keep the graph fresh after
|
|
197
|
+
pushes.
|
|
198
|
+
|
|
199
|
+
**Who can use your hosted URL — and what they get.** Anyone you share
|
|
200
|
+
`https://graph.yourdomain.com/mcp/<token>` with can add it as a custom
|
|
201
|
+
connector in their own Claude (Settings → Connectors → Add custom connector;
|
|
202
|
+
available on all plans — Free accounts can add one custom connector). But be
|
|
203
|
+
clear about the model: one running instance indexes **one repo on your
|
|
204
|
+
server**, and every connected user queries **that same shared graph**. It is
|
|
205
|
+
not multi-tenant — users can't point your instance at their own codebases.
|
|
206
|
+
Host it to demo the tool or to give a team a shared brain for one codebase;
|
|
207
|
+
for their own repos, users run their own instance (stdio locally via the
|
|
208
|
+
install command, or `--http` on their own box).
|
|
209
|
+
|
|
210
|
+
## Tools
|
|
211
|
+
|
|
212
|
+
| Tool | Purpose | Key params |
|
|
213
|
+
|---|---|---|
|
|
214
|
+
| `repo_summary` | Packages, deps, FE/BE routes, counts, session token savings | `detail`, `max_tokens` |
|
|
215
|
+
| `find_component_relations` | Everything related to a component/route: children, parents, hooks, endpoints, imports | `name`, `detail` |
|
|
216
|
+
| `map_frontend_to_backend` | Full chain `Component → HTTP call → backend route → handler`; works from either end | `name` (component, route, or `/path`) |
|
|
217
|
+
| `visualize_graph` | Mermaid/DOT slice: `component-tree` or `fe-to-be` | `name`, `kind`, `format`, `max_nodes` |
|
|
218
|
+
| `search_symbols` | Fuzzy find any node by name/path — replaces grep for "where is X" | `query`, `types`, `limit` |
|
|
219
|
+
| `impact_of_change` | Blast radius: transitive dependents of a node or file | `name`, `depth` |
|
|
220
|
+
| `reindex` | Refresh graph; incremental by default | `full` |
|
|
221
|
+
|
|
222
|
+
Every tool accepts `max_tokens` (response budget) and, where lists appear,
|
|
223
|
+
`detail: "compact" | "full"`.
|
|
224
|
+
|
|
225
|
+
Example output (`map_frontend_to_backend`, `name: "LoginPage"`):
|
|
226
|
+
|
|
227
|
+
```
|
|
228
|
+
LoginPage -> POST /api/auth/login -> POST /api/auth/login (services/api/app/auth.py) -> auth.login
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Example diagram (`visualize_graph`, `kind: "fe-to-be"`):
|
|
232
|
+
|
|
233
|
+
```mermaid
|
|
234
|
+
graph LR
|
|
235
|
+
n1(("route /login")) --> n2["LoginPage"]
|
|
236
|
+
n2 -- calls --> n3["POST /api/auth/login"]
|
|
237
|
+
n3 -- hits --> n4[/"POST /api/auth/login"/]
|
|
238
|
+
n4 -- handled by --> n5[["fn login"]]
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Storage format
|
|
242
|
+
|
|
243
|
+
`.codegraph/graph.json` — versioned node/edge lists, loaded into in-memory
|
|
244
|
+
Maps (by id, name, file, endpoint path) at startup; a ~1k-file repo loads in
|
|
245
|
+
well under a second. `.codegraph/manifest.json` — sha1 per indexed file,
|
|
246
|
+
drives incremental re-index and deleted-file pruning. Corrupted or missing
|
|
247
|
+
graph files trigger a clean full re-index, never a crash.
|
|
248
|
+
|
|
249
|
+
## Development
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
npm install
|
|
253
|
+
npm test # 57 unit assertions against a bundled fixture mono-repo,
|
|
254
|
+
# an MCP smoke test speaking real JSON-RPC over stdio,
|
|
255
|
+
# and an HTTP smoke test covering --http mode + token auth
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
The fixture under `test/fixture/` is a miniature mono-repo (React app, FastAPI
|
|
259
|
+
service, Express service) exercising every detection path including
|
|
260
|
+
`include_router` alias resolution and template-literal endpoints.
|
|
261
|
+
|
|
262
|
+
## Publishing this repo to GitHub
|
|
263
|
+
|
|
264
|
+
Credentials never leave your machine, so push it yourself:
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
cd code-graph-mcp
|
|
268
|
+
git init && git add -A && git commit -m "code-graph-mcp v0.2.0"
|
|
269
|
+
git branch -M main
|
|
270
|
+
git remote add origin https://github.com/dorkian/code-graph-mcp.git
|
|
271
|
+
git push -u origin main
|
|
272
|
+
|
|
273
|
+
# optional: publish to npm so npx works
|
|
274
|
+
npm login
|
|
275
|
+
npm publish --access public
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## License
|
|
279
|
+
|
|
280
|
+
MIT © dorkian
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// code-graph-mcp CLI
|
|
3
|
+
// code-graph-mcp --repo <path> MCP server (stdio)
|
|
4
|
+
// code-graph-mcp --repo <path> --http [--port N] [--auth-token T]
|
|
5
|
+
// MCP server (Streamable HTTP)
|
|
6
|
+
// code-graph-mcp --repo <path> --index [--full] index only and exit
|
|
7
|
+
// code-graph-mcp install claude-code|cursor [--repo p] configure a client
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import { fullIndex, incrementalIndex } from "../src/indexer.js";
|
|
11
|
+
import { Graph } from "../src/graph.js";
|
|
12
|
+
import { startServer, startHttpServer } from "../src/server.js";
|
|
13
|
+
import { install } from "../src/install.js";
|
|
14
|
+
|
|
15
|
+
const args = process.argv.slice(2);
|
|
16
|
+
function flag(name) { return args.includes(name); }
|
|
17
|
+
function opt(name, dflt) {
|
|
18
|
+
const i = args.indexOf(name);
|
|
19
|
+
return i >= 0 && args[i + 1] ? args[i + 1] : dflt;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (flag("--help") || flag("-h")) {
|
|
23
|
+
console.log(`code-graph-mcp — local code-intelligence MCP server
|
|
24
|
+
|
|
25
|
+
Usage:
|
|
26
|
+
code-graph-mcp --repo <path> start MCP server (stdio)
|
|
27
|
+
code-graph-mcp --repo <path> --http start MCP server (Streamable HTTP)
|
|
28
|
+
[--port 3333] [--host 127.0.0.1] [--auth-token <secret>]
|
|
29
|
+
code-graph-mcp --repo <path> --index [--full] build/refresh index and exit
|
|
30
|
+
code-graph-mcp install claude-code [--repo <path>] write .mcp.json + copy skill
|
|
31
|
+
code-graph-mcp install cursor [--repo <path>] write .cursor/mcp.json
|
|
32
|
+
code-graph-mcp install print setup for other clients
|
|
33
|
+
|
|
34
|
+
The graph is stored at <repo>/.codegraph/ (add it to .gitignore).`);
|
|
35
|
+
process.exit(0);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const repo = path.resolve(opt("--repo", process.env.CODE_GRAPH_REPO ?? process.cwd()));
|
|
39
|
+
|
|
40
|
+
if (args[0] === "install") {
|
|
41
|
+
try { console.log(install(args[1], repo)); process.exit(0); }
|
|
42
|
+
catch (e) { console.error(`install failed: ${e.message}`); process.exit(1); }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!fs.existsSync(repo) || !fs.statSync(repo).isDirectory()) {
|
|
46
|
+
console.error(`[code-graph-mcp] not a directory: ${repo}`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (flag("--index")) {
|
|
51
|
+
const existing = flag("--full") ? null : Graph.load(repo);
|
|
52
|
+
const res = existing ? incrementalIndex(repo, existing) : fullIndex(repo);
|
|
53
|
+
const s = res.graph.stats();
|
|
54
|
+
console.log(`indexed (${res.mode}): ${s.nodes} nodes, ${s.edges} edges in ${res.ms}ms; warnings: ${res.warnings.length}`);
|
|
55
|
+
for (const w of res.warnings.slice(0, 10)) console.log(` warn: ${w}`);
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (flag("--http")) {
|
|
60
|
+
startHttpServer(repo, {
|
|
61
|
+
port: Number(opt("--port", 3333)),
|
|
62
|
+
host: opt("--host", "127.0.0.1"),
|
|
63
|
+
token: opt("--auth-token", null),
|
|
64
|
+
}).catch((e) => { console.error("[code-graph-mcp] fatal:", e); process.exit(1); });
|
|
65
|
+
} else {
|
|
66
|
+
startServer(repo).catch((e) => { console.error("[code-graph-mcp] fatal:", e); process.exit(1); });
|
|
67
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ashkand/code-graph-mcp",
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"description": "Local code-intelligence MCP server: index a mono-repo into a knowledge graph (routes, components, API calls, backend handlers) and answer structural questions in a fraction of the tokens.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"code-graph-mcp": "bin/code-graph-mcp.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "src/server.js",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node bin/code-graph-mcp.js",
|
|
12
|
+
"test": "node test/run-tests.js && node test/mcp-smoke.js && node test/http-smoke.js"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"mcp",
|
|
16
|
+
"model-context-protocol",
|
|
17
|
+
"code-intelligence",
|
|
18
|
+
"knowledge-graph",
|
|
19
|
+
"claude-code",
|
|
20
|
+
"cursor",
|
|
21
|
+
"monorepo",
|
|
22
|
+
"token-optimization"
|
|
23
|
+
],
|
|
24
|
+
"author": "dorkian",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/dorkian/code-graph-mcp.git"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/dorkian/code-graph-mcp#readme",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/dorkian/code-graph-mcp/issues"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"bin/",
|
|
39
|
+
"src/",
|
|
40
|
+
"skills/",
|
|
41
|
+
"README.md",
|
|
42
|
+
"LICENSE"
|
|
43
|
+
],
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
46
|
+
"zod": "^3.25.76"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: code-graph
|
|
3
|
+
description: >-
|
|
4
|
+
Answer questions about repo structure, architecture, and frontend-to-backend
|
|
5
|
+
flows by querying the code-graph-mcp tools instead of grepping and reading
|
|
6
|
+
files. Use this skill whenever the user asks where something is defined, what
|
|
7
|
+
uses or depends on a component, which backend endpoint or handler a page/route
|
|
8
|
+
hits, what a repo or package contains, what breaks if a file changes, or wants
|
|
9
|
+
a diagram of a component tree or FE→BE flow. Also use it BEFORE editing code
|
|
10
|
+
when adding a feature, to pick the right files with repo_summary and
|
|
11
|
+
find_component_relations first. Do NOT fall back to read_file/grep for these
|
|
12
|
+
structural questions while the code-graph MCP server is connected — read files
|
|
13
|
+
only after the graph has named the exact files worth reading.
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Code Graph
|
|
17
|
+
|
|
18
|
+
The `code-graph-mcp` server holds a pre-built knowledge graph of this repo:
|
|
19
|
+
packages, files, components, frontend routes, HTTP calls, backend routes, and
|
|
20
|
+
handler functions, with the edges between them. One tool call against the graph
|
|
21
|
+
replaces a grep plus several file reads — use it first, read files second.
|
|
22
|
+
|
|
23
|
+
## Question → tool mapping
|
|
24
|
+
|
|
25
|
+
| The user asks… | Call |
|
|
26
|
+
|---|---|
|
|
27
|
+
| "Where does login land in the backend?" / "Which handler serves /users/:id?" | `map_frontend_to_backend` with the component, route, or path |
|
|
28
|
+
| "What uses `Button`?" / "What does `Dashboard` depend on?" | `find_component_relations` |
|
|
29
|
+
| "What's in this repo?" / "Which packages/services exist?" | `repo_summary` |
|
|
30
|
+
| "Where is X defined?" | `search_symbols` (never grep for this) |
|
|
31
|
+
| "What breaks if I change X?" / before a refactor | `impact_of_change` |
|
|
32
|
+
| "Show me the component tree / the flow" | `visualize_graph` |
|
|
33
|
+
| Results look stale after edits | `reindex` (incremental, cheap) |
|
|
34
|
+
|
|
35
|
+
## Workflow: answering a structural question
|
|
36
|
+
|
|
37
|
+
1. Call the single most specific tool from the table — not `repo_summary`
|
|
38
|
+
first "for context" unless the question is actually repo-wide.
|
|
39
|
+
2. Answer from the tool output. Only `read_file` the specific files the graph
|
|
40
|
+
named, and only if the user needs code-level detail beyond structure.
|
|
41
|
+
3. If a name isn't found, the tool returns closest matches — pick the right
|
|
42
|
+
one and retry rather than switching to grep.
|
|
43
|
+
|
|
44
|
+
## Workflow: adding a feature or fixing a bug
|
|
45
|
+
|
|
46
|
+
1. `repo_summary` — identify the right package/service.
|
|
47
|
+
2. `find_component_relations` on the nearest existing component/route — see
|
|
48
|
+
its hooks, children, and endpoints so the new code matches local patterns.
|
|
49
|
+
3. `map_frontend_to_backend` if the change crosses the FE/BE boundary — know
|
|
50
|
+
the handler before touching the client call (and vice versa).
|
|
51
|
+
4. `impact_of_change` on anything you're about to modify — list the blast
|
|
52
|
+
radius and check the dependents after editing.
|
|
53
|
+
5. Only now read/edit the specific files identified. After substantial edits,
|
|
54
|
+
call `reindex` so later questions in the session stay accurate.
|
|
55
|
+
|
|
56
|
+
## Token discipline
|
|
57
|
+
|
|
58
|
+
- Default `detail: "compact"` is almost always enough; only pass
|
|
59
|
+
`detail: "full"` when the compact output was truncated on the part you need.
|
|
60
|
+
- Outputs end with `…+N more (refine query…)` when capped — refine (add
|
|
61
|
+
`types`, a more specific name) rather than immediately raising `max_tokens`.
|
|
62
|
+
- Don't re-call a tool you already called with the same arguments in this
|
|
63
|
+
conversation; the graph doesn't change unless files change.
|
|
64
|
+
|
|
65
|
+
## Diagrams
|
|
66
|
+
|
|
67
|
+
`visualize_graph` returns a ready `graph LR` Mermaid block (or DOT with
|
|
68
|
+
`format: "dot"`). Paste the Mermaid block into the reply inside a
|
|
69
|
+
```mermaid fenced code block so UIs that support it render the diagram; when
|
|
70
|
+
the interface can't render Mermaid, summarize the same slice as an indented
|
|
71
|
+
ASCII tree instead. Use `kind: "fe-to-be"` for request-flow questions and
|
|
72
|
+
`kind: "component-tree"` for UI hierarchy questions.
|
package/src/graph.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// Graph store for code-graph-mcp.
|
|
2
|
+
// Nodes: { id, type, name, file?, extra? }
|
|
3
|
+
// Edges: { from, to, type }
|
|
4
|
+
// Persisted as JSON (.codegraph/graph.json); indexed in memory for O(1) lookups.
|
|
5
|
+
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
|
|
9
|
+
export const NODE_TYPES = [
|
|
10
|
+
"package", "file", "component", "route", "endpoint-call", "api-route", "handler",
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export class Graph {
|
|
14
|
+
constructor(repoRoot) {
|
|
15
|
+
this.repoRoot = repoRoot;
|
|
16
|
+
this.nodes = new Map(); // id -> node
|
|
17
|
+
this.edges = []; // { from, to, type }
|
|
18
|
+
this.byName = new Map(); // lowercased name -> Set<id>
|
|
19
|
+
this.byFile = new Map(); // relPath -> Set<id> (nodes defined in file)
|
|
20
|
+
this.out = new Map(); // id -> [{to,type}]
|
|
21
|
+
this.in = new Map(); // id -> [{from,type}]
|
|
22
|
+
this.byEndpointPath = new Map(); // normalized path -> Set<id> (api-route + endpoint-call)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ---------- mutation ----------
|
|
26
|
+
addNode(node) {
|
|
27
|
+
this.nodes.set(node.id, node);
|
|
28
|
+
if (node.name) {
|
|
29
|
+
const key = node.name.toLowerCase();
|
|
30
|
+
if (!this.byName.has(key)) this.byName.set(key, new Set());
|
|
31
|
+
this.byName.get(key).add(node.id);
|
|
32
|
+
}
|
|
33
|
+
if (node.file) {
|
|
34
|
+
if (!this.byFile.has(node.file)) this.byFile.set(node.file, new Set());
|
|
35
|
+
this.byFile.get(node.file).add(node.id);
|
|
36
|
+
}
|
|
37
|
+
if ((node.type === "api-route" || node.type === "endpoint-call") && node.extra?.normPath) {
|
|
38
|
+
const k = node.extra.normPath;
|
|
39
|
+
if (!this.byEndpointPath.has(k)) this.byEndpointPath.set(k, new Set());
|
|
40
|
+
this.byEndpointPath.get(k).add(node.id);
|
|
41
|
+
}
|
|
42
|
+
return node;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
addEdge(from, to, type) {
|
|
46
|
+
if (!this.nodes.has(from) || !this.nodes.has(to)) return;
|
|
47
|
+
this.edges.push({ from, to, type });
|
|
48
|
+
if (!this.out.has(from)) this.out.set(from, []);
|
|
49
|
+
this.out.get(from).push({ to, type });
|
|
50
|
+
if (!this.in.has(to)) this.in.set(to, []);
|
|
51
|
+
this.in.get(to).push({ from, type });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Remove every node defined in `relPath` (and its edges). Used by incremental reindex.
|
|
55
|
+
removeFile(relPath) {
|
|
56
|
+
const ids = this.byFile.get(relPath);
|
|
57
|
+
if (!ids) return 0;
|
|
58
|
+
const dead = new Set(ids);
|
|
59
|
+
// file node itself uses id `file:<rel>`
|
|
60
|
+
dead.add(`file:${relPath}`);
|
|
61
|
+
for (const id of dead) {
|
|
62
|
+
const n = this.nodes.get(id);
|
|
63
|
+
if (!n) continue;
|
|
64
|
+
this.nodes.delete(id);
|
|
65
|
+
if (n.name) this.byName.get(n.name.toLowerCase())?.delete(id);
|
|
66
|
+
if (n.extra?.normPath) this.byEndpointPath.get(n.extra.normPath)?.delete(id);
|
|
67
|
+
}
|
|
68
|
+
this.byFile.delete(relPath);
|
|
69
|
+
const before = this.edges.length;
|
|
70
|
+
this.edges = this.edges.filter((e) => !dead.has(e.from) && !dead.has(e.to));
|
|
71
|
+
this.rebuildAdjacency();
|
|
72
|
+
return before - this.edges.length;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
rebuildAdjacency() {
|
|
76
|
+
this.out = new Map();
|
|
77
|
+
this.in = new Map();
|
|
78
|
+
for (const e of this.edges) {
|
|
79
|
+
if (!this.out.has(e.from)) this.out.set(e.from, []);
|
|
80
|
+
this.out.get(e.from).push({ to: e.to, type: e.type });
|
|
81
|
+
if (!this.in.has(e.to)) this.in.set(e.to, []);
|
|
82
|
+
this.in.get(e.to).push({ from: e.from, type: e.type });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---------- queries ----------
|
|
87
|
+
findByName(name) {
|
|
88
|
+
return [...(this.byName.get(name.toLowerCase()) ?? [])].map((id) => this.nodes.get(id));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Fuzzy: substring + subsequence scoring across node names and file paths.
|
|
92
|
+
fuzzy(query, limit = 8, types = null) {
|
|
93
|
+
const q = query.toLowerCase();
|
|
94
|
+
const scored = [];
|
|
95
|
+
for (const n of this.nodes.values()) {
|
|
96
|
+
if (types && !types.includes(n.type)) continue;
|
|
97
|
+
const hay = `${n.name ?? ""} ${n.file ?? ""}`.toLowerCase();
|
|
98
|
+
let score = 0;
|
|
99
|
+
const nameLc = (n.name ?? "").toLowerCase();
|
|
100
|
+
if (nameLc === q) score = 100;
|
|
101
|
+
else if (nameLc.startsWith(q)) score = 80;
|
|
102
|
+
else if (nameLc.includes(q)) score = 60;
|
|
103
|
+
else if (hay.includes(q)) score = 40;
|
|
104
|
+
else if (isSubsequence(q, nameLc)) score = 20;
|
|
105
|
+
if (score > 0) scored.push([score, n]);
|
|
106
|
+
}
|
|
107
|
+
scored.sort((a, b) => b[0] - a[0] || (a[1].name ?? "").length - (b[1].name ?? "").length);
|
|
108
|
+
return scored.slice(0, limit).map(([, n]) => n);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
neighbors(id, dir = "out", edgeTypes = null) {
|
|
112
|
+
const list = (dir === "out" ? this.out : this.in).get(id) ?? [];
|
|
113
|
+
return list
|
|
114
|
+
.filter((e) => !edgeTypes || edgeTypes.includes(e.type))
|
|
115
|
+
.map((e) => ({ node: this.nodes.get(dir === "out" ? e.to : e.from), type: e.type }))
|
|
116
|
+
.filter((e) => e.node);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// BFS over reverse edges: who depends (directly or transitively) on `id`.
|
|
120
|
+
dependents(id, maxDepth = 3, edgeTypes = ["imports", "renders", "calls", "hits-endpoint", "handled-by"]) {
|
|
121
|
+
const seen = new Map(); // id -> depth
|
|
122
|
+
let frontier = [id];
|
|
123
|
+
for (let d = 1; d <= maxDepth && frontier.length; d++) {
|
|
124
|
+
const next = [];
|
|
125
|
+
for (const cur of frontier) {
|
|
126
|
+
for (const { node } of this.neighbors(cur, "in", edgeTypes)) {
|
|
127
|
+
if (node.id === id || seen.has(node.id)) continue;
|
|
128
|
+
seen.set(node.id, d);
|
|
129
|
+
next.push(node.id);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
frontier = next;
|
|
133
|
+
}
|
|
134
|
+
return [...seen.entries()].map(([nid, depth]) => ({ node: this.nodes.get(nid), depth }));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
stats() {
|
|
138
|
+
const byType = {};
|
|
139
|
+
for (const n of this.nodes.values()) byType[n.type] = (byType[n.type] ?? 0) + 1;
|
|
140
|
+
return { nodes: this.nodes.size, edges: this.edges.length, byType };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ---------- persistence ----------
|
|
144
|
+
static storageDir(repoRoot) { return path.join(repoRoot, ".codegraph"); }
|
|
145
|
+
static graphPath(repoRoot) { return path.join(Graph.storageDir(repoRoot), "graph.json"); }
|
|
146
|
+
static manifestPath(repoRoot) { return path.join(Graph.storageDir(repoRoot), "manifest.json"); }
|
|
147
|
+
|
|
148
|
+
save() {
|
|
149
|
+
const dir = Graph.storageDir(this.repoRoot);
|
|
150
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
151
|
+
const payload = { version: 1, nodes: [...this.nodes.values()], edges: this.edges };
|
|
152
|
+
fs.writeFileSync(Graph.graphPath(this.repoRoot), JSON.stringify(payload));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** @returns {Graph|null} null when missing or corrupted (caller falls back to full index — spec E6) */
|
|
156
|
+
static load(repoRoot) {
|
|
157
|
+
try {
|
|
158
|
+
const raw = fs.readFileSync(Graph.graphPath(repoRoot), "utf8");
|
|
159
|
+
const data = JSON.parse(raw);
|
|
160
|
+
if (data.version !== 1 || !Array.isArray(data.nodes) || !Array.isArray(data.edges)) return null;
|
|
161
|
+
const g = new Graph(repoRoot);
|
|
162
|
+
for (const n of data.nodes) g.addNode(n);
|
|
163
|
+
for (const e of data.edges) g.addEdge(e.from, e.to, e.type);
|
|
164
|
+
return g;
|
|
165
|
+
} catch {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function isSubsequence(needle, hay) {
|
|
172
|
+
let i = 0;
|
|
173
|
+
for (const c of hay) if (c === needle[i]) i++;
|
|
174
|
+
return i === needle.length;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Shorten a list of repo-relative paths by stripping their longest common dir prefix.
|
|
178
|
+
export function shortenPaths(paths) {
|
|
179
|
+
if (paths.length < 2) return { prefix: "", paths };
|
|
180
|
+
const split = paths.map((p) => p.split("/"));
|
|
181
|
+
let common = 0;
|
|
182
|
+
outer: while (true) {
|
|
183
|
+
const seg = split[0][common];
|
|
184
|
+
if (seg === undefined) break;
|
|
185
|
+
for (const s of split) {
|
|
186
|
+
if (s.length - 1 <= common || s[common] !== seg) break outer;
|
|
187
|
+
}
|
|
188
|
+
common++;
|
|
189
|
+
}
|
|
190
|
+
if (common === 0) return { prefix: "", paths };
|
|
191
|
+
const prefix = split[0].slice(0, common).join("/") + "/";
|
|
192
|
+
return { prefix, paths: paths.map((p) => p.slice(prefix.length)) };
|
|
193
|
+
}
|