Code-Dendrite MCP server
Commit Oct 30, 2026Enables AI agents to navigate codebases using AST-based static analysis and call graphs instead of vector similarity search.
Also known as lo2589/Code-Dendrite
- Stars
- 0
- Tools
- —
- Downloads, 30 days
- —
- Latest version
- —
- Last commit
- Oct 30, 2026
- License
- MIT
What is Code-Dendrite MCP?
Code-Dendrite enables AI agents to navigate codebases using AST-based static analysis and call graphs instead of vector similarity search.
Summary generated from the project's README
About Code-Dendrite MCP server
Code-Dendrite provides a method for AI agents to navigate complex codebases using static analysis and call graphs. Unlike traditional retrieval methods that rely on vector similarity search to find code that looks alike, this tool parses repositories into a graph of causal edges. This allows an agent to follow actual connections between functions, even when the text of the calling code does not resemble the target function.
Developers can use this server locally by installing the cdd command via pip or running it from source. To use it with an MCP client, you must first index the project using the rebuild command. The server then exposes various layers of the codebase through tools. It is built for Python 3.10 or higher and uses the standard library for its core parsing logic. For non-Python languages, it employs lightweight pattern-matching adapters to identify symbols and call edges.
What you can do with it
- 1Trace function execution pathsAsk an agent to show how a specific entry function operates within a repository.
- 2Identify code callersDetermine which parts of a codebase are responsible for calling a specific function.
- 3Prepare model contextGenerate a markdown file containing a chain of code and its source for feeding into an AI model.
- 4Analyze project topologyPerform a full rebuild of the codebase index to map out structural connections.
Written from the project's README. Every use case is checked against a quote from it.
README
View on GitHubEnglish | 简体中文
Install in one line, then ask your agent
npx skills add lo2589/Code-Dendrite # 3 agent skills: understand · learn · rebuild
pip install git+https://github.com/lo2589/Code-Dendrite # the cdd command (Python ≥ 3.10, no dependencies)
Then tell your agent:
Use cdd-understand to show me how <your entry function> works in this repo.
CLI demo: rg vs cdd

The same coding task is shown side by side: rg on the left and cdd on the right. The animation plays directly in this README.
Static analysis instead of vector search.
Retrieval finds code that looks alike. Navigation needs code that is connected. A function that calls yours looks nothing like yours in text — which is exactly where similarity search is bound to fail on call-chain questions. CodeDendrite doesn't guess at similarity: it parses a repository into a graph of causal edges and lets you walk along them.
Status
| Item | Value |
|---|---|
| Layout | Five layers under src/; src/cdd_mcp/ is the MCP server; skills/ sits next to src/ |
| Requirements | Python ≥ 3.10; the core uses the standard library only |
| Parsing | Python through the standard-library ast module; every other language through lightweight, dependency-free pattern-matching adapters, so their symbols and call edges are heuristic. Depth per language: language support matrix |
| Index | Not committed to git. After cloning, run cdd rebuild first; see counts with cdd count / cdd status |
Installation
Option 1: run from source (no install)
git clone https://github.com/lo2589/Code-Dendrite
cd Code-Dendrite
python3 src/usage/cli/cdd rebuild . --virtual # build the index first: every read command except peek needs it
python3 src/usage/cli/cdd count
src/usage/cli/cdd puts src/ on the subprocess PYTHONPATH by itself; no extra setup is needed.
Option 2: install as the cdd command
pip install . # use pip install -e . for development
cdd rebuild /path/to/project --virtual
Optional dependencies (declared in pyproject.toml):
pip install '.[mcp]' # the MCP server needs the mcp package
pip install '.[test]' # running the tests needs pytest
MCP server: once mcp is installed, add this to .mcp.json:
{
"codedendrite": {
"command": "cdd-mcp",
"args": ["--root", "/path/to/project"]
}
}
The project that --root points to must have been indexed with cdd rebuild.
lens plugin (optional, only useful with DeepSeek Harness): lens/scripts/install.sh. Override the harness repo path, profile and port with DSH_REPO, PROFILE and DSH_PORT. The node path and the harness restart script inside the script are hard-coded to the author's machine; edit those two lines before using it elsewhere.
Layout
| Path | Contents |
|---|---|
src/scanner/ |
Language scanner: source → symbols / line numbers / imports / call sites. language/ (23 extensions routed by file extension across 23 LanguageSpecs — actual capability depth per spec is measured by the language support matrix below; 17 specs reach call-edge depth, 3 stop at symbol detection, 3 are recognized but not parsed), calls/, hashing/, project/, store/, structure/ |
src/analyzer/ |
Analyzer: symbols → anchor identity + causal edges. identity/ (structural hashing and drift), anchors/, inherit/, dataflow/, dynamic/, path/, control_flow/, ui/, shared/ |
src/api/ |
Query surface: inspector/, navigator/, query/, relations/ |
src/usage/ |
Usage surface: cli/, packets/, workflow/, profiles/, tools/, repair/, structure/, analyze/ |
src/view/ |
Visualization: browser/, code_map/, relations/, timeline/, module_tree/, pipeline/, html/ |
lens/ |
DSH plugin dsh-cdd-lens: a session trajectory sidebar + an interactive anchor graph (a node package, independent of src/) |
lens-pcb/ |
A parallel branch of lens (PCB-style layout) with its own build stamp |
src/cdd_mcp/ |
MCP server: exposes the layers above as tools |
skills/ |
Three agent skills: cdd-understand (understand a project), cdd-rebuild (topology refactoring), cdd-learn (layer-by-layer guided learning) |
src/tests/ |
Tests and fixtures |
Dependency direction: scanner → analyzer → api → usage / view. scanner and analyzer import nothing above them. One reverse dependency remains: api/query/cmd_chain.py imports view.relations, view.timeline, view.code_map and usage.profiles.yaml_profile inside functions (4 imports). Modules that used to sit in the wrong layer have moved, so scan_project, debug_workflow, generate_module_tree, smart_analyze and the navigator (start, jump, …) are imported from usage, and generate_mermaid_for_project from view.html.graph, not from api.
From entry point to implementation
The entry point is src/cdd_mcp/server.py::call_tool. Once the index is built, one command drills it down to the implementation layer:
python3 src/usage/cli/cdd chain "src/cdd_mcp/server.py::call_tool" --depth 5
The chain crosses three layers: mcp → usage → analyzer. The output is a tree expansion: a function called from several places is printed once at each place, so "lines" and "distinct functions" are two different numbers:
| Layer | Lines | Distinct functions |
|---|---|---|
src/cdd_mcp/ |
15 | 10 |
src/usage/ |
36 | 13 |
src/analyzer/ |
4 | 1 |
| Total | 55 | 24 |
The most repeated function is _anchor_attr (14 times). Full output (all 55 lines):
Chain from 'src/cdd_mcp/server.py::call_tool' (55 entries, direction=down):
[0] call_tool src/cdd_mcp/server.py:294
[1] tool_context src/cdd_mcp/server.py:84
[2] _anchors src/cdd_mcp/server.py:47
[3] load_anchors src/analyzer/anchors/core_load.py:33
[2] _relations src/cdd_mcp/server.py:54
[2] _module_tree src/cdd_mcp/server.py:62
[2] resolve_task_anchor src/usage/packets/task_context_packet/anchor.py:113
[3] _anchor_attr src/usage/packets/task_context_packet/anchor.py:54
[3] _bounded_matches src/usage/packets/task_context_packet/anchor.py:78
[4] _anchor_attr src/usage/packets/task_context_packet/anchor.py:54
[3] _match_sort_key src/usage/packets/task_context_packet/anchor.py:68
[4] _anchor_attr src/usage/packets/task_context_packet/anchor.py:54
[4] _is_test_file src/usage/packets/task_context_packet/plugin.py:119
[2] build_level0 src/usage/packets/task_context_packet/anchor.py:275
[3] _anchor_attr src/usage/packets/task_context_packet/anchor.py:54
[2] build_level1 src/usage/packets/task_context_packet/anchor.py:336
[3] _find_anchor_area src/usage/packets/task_context_packet/anchor.py:328
[4] _anchor_attr src/usage/packets/task_context_packet/anchor.py:54
[3] _find_anchor_module src/usage/packets/task_context_packet/plugin.py:123
[2] build_level2 src/usage/packets/task_context_packet/anchor.py:422
[3] _build_edge_indexes src/usage/packets/task_context_packet/anchor.py:250
[3] _score_related_symbol src/usage/packets/task_context_packet/anchor.py:391
[4] _anchor_attr src/usage/packets/task_context_packet/anchor.py:54
[3] _anchor_attr src/usage/packets/task_context_packet/anchor.py:54
[3] _find_anchor_module src/usage/packets/task_context_packet/plugin.py:123
[1] tool_symbol src/cdd_mcp/server.py:112
[2] _anchors src/cdd_mcp/server.py:47
[3] load_anchors src/analyzer/anchors/core_load.py:33
[2] _relations src/cdd_mcp/server.py:54
[2] resolve_task_anchor src/usage/packets/task_context_packet/anchor.py:113
[3] _anchor_attr src/usage/packets/task_context_packet/anchor.py:54
[3] _bounded_matches src/usage/packets/task_context_packet/anchor.py:78
[4] _anchor_attr src/usage/packets/task_context_packet/anchor.py:54
[3] _match_sort_key src/usage/packets/task_context_packet/anchor.py:68
[4] _anchor_attr src/usage/packets/task_context_packet/anchor.py:54
[4] _is_test_file src/usage/packets/task_context_packet/plugin.py:119
[2] build_anchor_packet src/usage/packets/task_context_packet/anchor.py:622
[3] _build_edge_indexes src/usage/packets/task_context_packet/anchor.py:250
[3] _anchor_attr src/usage/packets/task_context_packet/anchor.py:54
[1] tool_search src/cdd_mcp/server.py:126
[2] _anchors src/cdd_mcp/server.py:47
[3] load_anchors src/analyzer/anchors/core_load.py:33
[2] resolve_task_anchor src/usage/packets/task_context_packet/anchor.py:113
[3] _anchor_attr src/usage/packets/task_context_packet/anchor.py:54
[3] _bounded_matches src/usage/packets/task_context_packet/anchor.py:78
[4] _anchor_attr src/usage/packets/task_context_packet/anchor.py:54
[3] _match_sort_key src/usage/packets/task_context_packet/anchor.py:68
[4] _anchor_attr src/usage/packets/task_context_packet/anchor.py:54
[4] _is_test_file src/usage/packets/task_context_packet/plugin.py:119
[1] tool_impact src/cdd_mcp/server.py:150
[2] _relations src/cdd_mcp/server.py:54
[2] _anchors src/cdd_mcp/server.py:47
[3] load_anchors src/analyzer/anchors/core_load.py:33
[1] tool_modules src/cdd_mcp/server.py:194
[2] _project_summary src/cdd_mcp/server.py:73
This chain is computed from the index, not copied by hand: the numbers above come from the index as of 2026-09-14. Running the same command should give the same starting point and the same set of files; line counts, function counts and line numbers change with the code.
Commands
cdd is src/usage/cli/cdd (a Python script without an extension, run via its shebang). The examples below run it from source; after installing, replace python3 src/usage/cli/cdd with cdd.
cd <repo>
python3 src/usage/cli/cdd status # index location, counts, timestamps
python3 src/usage/cli/cdd rebuild . --virtual # full rebuild
python3 src/usage/cli/cdd update . # incremental update; builds fully the first time
python3 src/usage/cli/cdd count # count of each causal edge type
python3 src/usage/cli/cdd peek <file> # parse one file on the spot, no index needed
python3 src/usage/cli/cdd chain <anchor> --depth 12
python3 src/usage/cli/cdd callers <anchor> # upward: who uses it
python3 src/usage/cli/cdd chain2context <anchor> --budget 40000 --output ctx.md
python3 src/usage/cli/cdd draw # generate the browser-based directory explorer
python3 src/usage/cli/cdd modules <keyword> # semantic modules
chain2context assembles every anchor on a chain, together with its source code, into context ready to feed to a model. Anchors that exceed the budget are listed in a separate section instead of being silently dropped.
Five causal relations
Counts vary with the code and the scan scope; use cdd count for current values.
| Type | Meaning |
|---|---|
call |
A calls B |
dataflow |
A's output flows into B's input |
impact |
≥2 modules depend on the same target, so changing it affects all of them |
inherit |
A subclass inherits from a parent class |
dispatch |
Registry / decorator / HTTP route dispatch |
Edges are built only when they can be resolved statically. Nothing is invented for what can't be resolved: when cdd callers finds zero callers for a method, it reports the unresolved candidate call sites instead of claiming "nobody uses it".
Language support matrix
The scanner routes 23 file extensions through src/scanner/language/registry.py. They are not all parsed at the same depth — capability is split into three tiers, each measured by src/tests/tests/test_lang_support_matrix_v1_48.py against the fixtures in src/tests/fixtures/lang_matrix/. The matrix table below is guarded by a drift test (test_readme_matrix_matches_fixtures): changing a tier requires updating the test in lock-step.
| Tier | Capability | Specs (count) | What you get |
|---|---|---|---|
| T1 | Symbols + call edges | 17 | parse_paths yields ≥2 anchors per fixture; the caller anchor records a helper callee |
| T2 | Symbols only (generic / component) | 3 | ≥1 anchor; no call-edge guarantee |
| T3 | Recognized but not parsed | 3 | language_for_path returns a spec; parse_paths produces zero anchors (yaml / json / toml — they exist so the indexer doesn't trip on them, but the analyzer is None) |
| Tier | Spec | Fixture | Anchor count (measured) |
|---|---|---|---|
| T1 | python | a.py |
2 |
| T1 | typescript | b.ts |
2 |
| T1 | javascript | c.js |
2 |
| T1 | cpp | d.cpp |
2 |
| T1 | java | e.java |
3 |
| T1 | kotlin | f.kt |
2 |
| T1 | go | g.go |
2 |
| T1 | rust | h.rs |
2 |
| T1 | swift | i.swift |
2 |
| T1 | php | j.php |
2 |
| T1 | ruby | k.rb |
2 |
| T1 | html | l.html |
2 |
| T1 | csharp | m.cs |
3 |
| T1 | shell | p.sh |
2 |
| T1 | powershell | q.ps1 |
2 |
| T1 | vue | r.vue |
2 |
| T1 | svelte | s.svelte |
2 |
| T2 | scala | n.scala |
1 |
| T2 | objective-c | o.m |
3 |
| T2 | sql | t.sql |
2 |
| T3 | yaml | u.yaml |
0 |
| T3 | json | v.json |
0 |
| T3 | toml | w.toml |
0 |
When you change an adapter's depth — adding call edges to scala, teaching the indexer about yaml structure, splitting cpp into a real parser — the test file's tier table is the contract: move the spec, fix the fixture, and the matrix here follows.
Anchor granularity
Anchors go down to module-level defs and class methods only. A def nested inside a function body (a local helper, a closure factory) does not get its own anchor: its call edges are recorded under the enclosing function's callees, and opening the enclosing function's source gives you all of it — there is no separate identity worth querying. Three examples of this rule:
_coerce_callers/_coerce_calleesinsidesrc/analyzer/anchors/core_load.py::load_anchors— the index has no anchor entries for them, butload_anchors.calleeskeeps string traces of them_relatedinsidesrc/usage/packets/task_context_packet/anchor.py::build_anchor_packet— same as above- This is not a missed detection; it is a design choice: the anchor generator only walks
tree.body(module level + class-method level)
AML Coding memory 0.2 (src/usage/aml_coding)
An Add/Search service for the Agent Memory Leaderboard Coding track. Its only model is text-embedding-v4: it makes no LLM calls and uses no reranker, and it never clones a repository.
Add receives request_id, user_id, session_id, messages[{role, content, timestamp?}], one solved coding task per request. It processes the task in these steps:
- Fingerprint the trajectory. A trajectory already stored for another
user_idcosts only a reference row. - Parse the messages into events: the issue, thinking, tool calls and their results. Results that were split across messages are glued back together.
- Replay the files in time order.
Readrecords the numbered lines,Editappliesold_string → new_stringand records where it landed, andWritereplaces the file. - Extract anchors (
file::Class.methodwith line spans) with Pythonast, together with same-file call edges. Files that were only read in pieces fall back todef/classlines. - Link the task's
read/edit/test/ qualifiedmentionevents to those anchors. - Build one unit per touched anchor, holding the code, its calls and callers, and the task's diff (at most about 1,500 tokens). Everything else becomes 100-word chunks with a stride of 80.
- Embed only the plain chunks, then write everything in one transaction.
Add answers 200 only after the write is durable. The same request_id with the same body is a no-op; with a different body it returns 409. Caps: 2 MB of replayed code and 2,000 anchors per trajectory, and 503 when less than 3 GB of disk is free.
Search receives query, user_id, top_k. It searches only inside the trajectories that this user_id references, through three routes: exact symbol and path hits (weight 3), BM25 over all units, and vector similarity over the plain chunks. The routes are fused by weighted reciprocal rank. Each anchor appears once. Other tasks that touched the same symbol are listed as pointers only: newest first, without their code, and using at most 20% of the budget. A reply holds at most top_k items and about 12k tokens, as {"data": [{id, content, score, created_at}]}. It returns evidence only and never a generated answer.
mkdir -p ~/.config/cdd-aml && (umask 077; cat > ~/.config/cdd-aml/dashscope.key) # Model Studio key
cd src && python3 -m usage.aml_coding serve --data ~/cdd-aml-data --key-file <file with the AML API key> --port 8014
python3 -m usage.aml_coding stats --data ~/cdd-aml-data # calls, embedding tokens, cost
Origin: the service is the author's own original work and is built on CodeDendrite's anchor model. No third-party code is reused. The only runtime dependency is numpy, and the only model called is text-embedding-v4.
Tests
cd <repo>
pip install '.[test]'
PYTHONPATH=src python3 -m pytest src/tests src/tests/tests -q
Generated artifacts
These are build outputs. Don't edit them by hand; regenerate them with cdd rebuild:
.codedendrite/— anchors.json / relations.json / module_tree*.json / various .html files.code_genome_db.json,.code_hash_db.json(with.lockfiles)
.codedendrite_ignore controls what the scan excludes (currently .cache/, backup/, doc/, the lib/ build output and design/ mockups of lens/ and lens-pcb/, and lens/output/).
Code-Dendrite MCP questions
How does CodeDendrite differ from vector search?
Unlike retrieval which finds code that looks similar, CodeDendrite parses repositories into a graph of causal edges to provide connected navigation.
What are the system requirements for CodeDendrite?
It requires Python 3.10 or higher and the core functionality uses only the standard library.
Do I need to build an index before using the MCP server?
Yes, the project must be indexed using the rebuild command before the MCP server can be used.