Cartograph
Cartograph
Агент-ориентированный интеллект для кода. Превратите любой репозиторий в запрашиваемый граф кода и отдайте его агентам через MCP — чтобы агент мог спросить: «что сломается, если я изменю это?», а не грепать и надеяться.
tree-sitter + SQLite. Без эмбеддингов, без векторного хранилища, без API-ключей, без сервера, без затрат.
→ Живое демо — создаётся из реального индекса этого репозитория при каждом пуше.
Проблема
Дайте агенту программирования большой незнакомый репозиторий и посмотрите, что он делает: grep, читает файл, снова grep, читает другой файл. Он сжигает контекст, восстанавливая структуру, которую парсер мог бы выдать за один вызов, — и всё равно упускает вызывающий код на три модуля дальше, который его изменение сломало.
Обычное решение — RAG: построить эмбеддинги кодовой базы и получать «похожие» фрагменты. Но «кто вызывает эту функцию?» — это не вопрос похожести. У него есть точный ответ, и этот ответ находится в графе вызовов.
Cartograph строит граф, а затем даёт агентам десять инструментов, заточенных под то, как они реально работают.
$ cartograph blast src/cartograph/graph/store.py
## Blast radius — file `src/cartograph/graph/store.py`
17 dependent file(s), 31 affected symbol(s), 7 test file(s).
**Tests to run first**
- `tests/test_cli.py`
- `tests/test_docs.py`
- `tests/test_incremental.py`
- `tests/test_mcp.py`
- `tests/test_resolver.py`
- `tests/test_traversal.py`
- `tests/test_views.py`
**Dependent files** (by import distance)
- `src/cartograph/graph/resolver.py` · d1
- `src/cartograph/indexer/pipeline.py` · d1
- `src/cartograph/service.py` · d1
- `src/cartograph/cli.py` · d2
…Один вызов — до правки. Не семь грепов после того, как тест-сьют покраснел.
Быстрый старт
uv tool install cartograph-mcp # or: pipx install cartograph-mcp
cartograph index ~/code/my-repo # builds .cartograph/cartograph.db
cartograph arch # modules, layers, cycles, hotspots
cartograph blast src/auth/token.py # what a change here could break
cartograph callers validate_token # reverse call treeПодключите к агенту
Claude Code:
claude mcp add cartograph -- cartograph serve /path/to/repoИли любой MCP-клиент через mcp.json:
{
"mcpServers": {
"cartograph": {
"command": "cartograph",
"args": ["serve", "/path/to/repo"]
}
}
}serve индексирует при первом запуске, если индекс ещё не создан. Затем спросите агента: «что сломается, если я изменю валидатор токена?» — и он вызовет blast_radius вместо того, чтобы гадать.
Десять инструментов
Инструмент | Ответ |
| Где определён X? (ранжировано по структурной значимости) |
| Полнотекстовый поиск по именам, сигнатурам, докстрингам (BM25) |
| Один символ: сигнатура, документация, члены, вызывающие, вызываемые, исходный код |
| Обратное дерево вызовов — перед изменением сигнатуры |
| Прямое дерево вызовов — понять код, не читая каждый файл |
| Что может сломать изменение, и какие тесты запускать |
| «Что ещё почитать?» через персонализированный PageRank |
| Что файл определяет, что импортирует и кто его импортирует |
| Модули, слои, циклы импортов, горячие точки, точки входа |
| Здоровье индекса и разбивка разрешения рёбер по правилам |
Плюс MCP-ресурсы (cartograph://architecture, cartograph://stats) и промпт orient для первичного прохода по незнакомому репозиторию через граф.
Языки: Python, TypeScript, TSX, JavaScript, Go.
Проектные решения, о которых стоит поспорить
1. Уверенность — полноправная колонка
Без тайпчекера нельзя знать, что store.who_calls() означает GraphStore.who_calls. Можно лишь ранжировать гипотезы. Поэтому вместо притворства каждое ребро записывает правило, которое его породило, и уверенность:
Правило | Уверенность | Смысл |
| 0.95 | определение прямо здесь, в области видимости |
| 0.90 | файл явно импортировал это имя |
| 0.85 |
|
| 0.75 | соседний файл в том же пакете |
| 0.60 | ровно один символ репозитория с таким именем, голый вызов |
| 0.45 | одно совпадение, но на нетипизированном приёмнике |
| ≤0.40 | N кандидатов, сохраняется как N рёбер по 1/N каждое |
| 0.00 | корень в импорте сторонней/стандартной библиотеки |
| 0.00 | действительно неизвестно (динамика или типизированный метод) |
Вызывающие код затем сами выбирают рабочую точку. who_calls по умолчанию использует порог ≥0,5 — сначала точность, потому что агент действует на основе ответа. blast_radius опускается до 0,3 — сначала полнота, потому что пропущенный затронутый тест — дорогая ошибка, а ложное срабатывание стоит лишь беглого взгляда ревьюера.
Ярус name-only существует из-за реального бага. seen.add(...) на встроенном set резолвился в метод add класса из репозитория только потому, что имя случайно оказалось уникальным, — и он выглядел уверенным вызывающим. Имя метода на приёмнике, который нельзя типизировать, не является доказательством, поэтому теперь оно оказывается ниже линии точности. (тест)
external существует для честности метрик: на большинстве репозиториев категорию «unresolved» заполняют в основном typer.Option и sqlite3.execute. Включение их в статистику делает покрытие гораздо хуже, чем оно есть, поэтому Cartograph сообщает внутреннее разрешение — из тех мест вызовов, которые могут указывать на символ репозитория, сколько реально указывают.
2. Парсинг инкрементален, резолвинг — никогда
Файл перечитывается, только когда меняется его sha256. Но сырые ссылки хранятся как факты в таблице refs, а edges пересчитывается как чистая функция (refs × symbols) при любом изменении.
Именно это делает «переиндексацию после каждой правки» надёжной. Если бы разрешение тоже было инкрементальным, правка одного файла могла бы оставить ребро в другом файле, указывающее на символ, который переехал. Полное повторное разрешение делает это структурно невозможным. (тест)
Стоимость реальна, поэтому есть ровно одно безопасное упрощение: если ни один файл не был добавлен, не перечитан и не удалён, обе входные таблицы не изменились, и разрешение доказуемо идентично — поэтому оно пропускается. Это сократило повторную индексацию Django без изменений с 7.5s до 0.67s и побайтно идентичным графом.
3. PageRank вместо эмбеддингов
«Какой get вы имели в виду?» — это структурный вопрос. Тот get, от которого зависят сорок мест вызовов, — тот, который нужен агенту, и граф вызовов это уже знает. Поэтому ранжирование символов — это взвешенный PageRank по графу вызовов: стабильный, объяснимый и бесплатный. Никакой модели, никакой сборки индекса, никакого векторного хранилища.
related_symbols развивает ту же идею: персонализированный PageRank, инициированный одним символом, с рассмотрением графа как неориентированного, потому что если вы собираетесь изменить функцию, и её вызывающие, и её вызываемые — релевантный контекст. Это структурный аналог семантического поиска, и ему не нужны эмбеддинги.
4. Инструменты возвращают Markdown, а не JSON, в рамках бюджета токенов
Потребитель — это контекстное окно. JSON-массив из 40 символов тратит тысячи токенов на скобки и повторяющиеся ключи, а модель в любом случае переформатирует его. Каждое представление здесь — компактный Markdown с жёстким бюджетом токенов.
Критически важно: каждое усечение объявляется. Агент, получивший 20 из 87 вызывающих без маркера, уверенно заключит, что остальных 67 не существует, и удалит что-нибудь.
5. Обход выполняется в SQLite, а не в Python
who_calls на глубине 4 — это рекурсивный CTE, поэтому весь обход остаётся внутри C-цикла SQLite. На графе Django с 252 тыс. рёбер это ~5 мс. Вытаскивание таблицы рёбер в Python для обхода не было бы таким.
Бенчмарки
Реальные репозитории, ноутбук на M-серии, один процесс. Холодный запуск — полный индекс с нуля; тёплый — повторная индексация без изменений.
Репозиторий | Файлы | KLOC | Символы | Рёбра | Холодный | Тёплый | БД | Внутреннее разрешение |
2,973 | 534 | 45,394 | 252,441 | 11.9s | 0.67s | 80 MB | 83.2% | |
gin (Go) | 98 | 24 | 1,610 | 9,179 | 0.32s | 0.03s | 2.5 MB | 88.1% |
83 | 18 | 1,624 | 4,271 | 0.21s | 0.03s | 1.7 MB | 87.4% |
Задержка запросов (медиана из 5, тёплый режим):
Репозиторий |
|
|
|
|
django | 12.3ms | 5.1ms | 5.6ms | 68.5ms |
gin | 0.4ms | 0.4ms | 0.5ms | 1.2ms |
flask | 0.5ms | 1.1ms | 1.3ms | 1.8ms |
Воспроизвести: scripts/bench.py.
Архитектура
flowchart LR
subgraph index["cartograph index"]
W[walker<br/>git ls-files] --> P[tree-sitter<br/>+ .scm queries]
P --> X[extract<br/>defs · refs · imports]
end
X --> DB[(SQLite<br/>symbols · refs<br/>edges · FTS5)]
DB --> R[resolver<br/>rule cascade]
R --> DB
DB --> RK[PageRank<br/>Tarjan SCC]
RK --> DB
DB --> S[service facade]
S --> V[views<br/>token-budgeted MD]
V --> M[MCP server<br/>10 tools]
V --> C[CLI]
M --> A((coding agent))Модуль | Назначение |
| Обнаружение файлов — использует |
| По одному адаптеру на язык: расширения, запросы, докстринги, ключи модулей, разрешение импортов |
| AST → символы/ссылки/импорты, без привязки к языку |
| tree-sitter паттерны захвата — знания по каждому языку в виде данных |
| Граф: |
| Каскад уверенности |
| PageRank, персонализированный PageRank, итеративный Tarjan SCC, слои |
| Обход через рекурсивные CTE, ранжированный поиск, агрегаты |
| Единый фасад, чтобы CLI и MCP-сервер не расходились |
| Markdown с бюджетом токенов |
Определение области видимости без комбинаторных запросов
Трюк, который сохраняет queries/*.scm небольшими: область видимости никогда не кодируется в запросе. Каждое захваченное определение индексируется по id узла tree-sitter, а охватывающий символ ссылки находится обходом цепочки parent, пока не встретится один. Это O(глубины дерева) на ссылку и без дополнительных усилий обрабатывает замыкания, методы, внутренние классы и стрелочные функции — без отдельных паттернов на каждую форму.
Добавление языка
Создайте подкласс LanguageAdapter (около 40 строк) и добавьте файл .scm. GoAdapter — самый короткий полный пример. tests/test_queries.py затем автоматически компилирует ваши запросы с грамматикой и проверяет, что они захватывают что-то.
Разработка
git clone https://github.com/GokulRaj2210/cartograph-mcp && cd cartograph-mcp
uv sync
uv run pytest -q # 209 tests
uv run ruff check .
uv run mypy # strictCI запускает набор тестов на Python 3.11/3.12/3.13 (плюс macOS), затем использует продукт на себе: индексирует этот репозиторий, падает при циклах импортов, проверяет, что повторная индексация без изменений не перечитывает ничего, и гоняет MCP-сервер через настоящий stdio. Он также устанавливает собранный wheel в чистый venv и индексирует с его помощью, потому что упакованные .scm файлы легко случайно не включить в wheel и невозможно заметить локально.
Проверка циклов уже оправдала себя — она поймала цикл store → resolver → store, который я добавил в этот репозиторий, и он был исправлен перемещением проблемного хелпера, а не ослаблением проверки.
Заметные тесты
tests/test_queries.py— каждый.scmкомпилируется с каждой грамматикой, которая его загружает, и что-то захватывает. Паттерн, допустимый в JavaScript ((class_heritage (identifier))), является невозможным паттерном в TypeScript, который оборачивает супертипы вextends_clause. Эта одна строка молча давала ноль символов TypeScript.tests/test_incremental.py— после правок, удалений или перемещения символа между файлами не остаётся устаревших рёбер.tests/test_resolver.py— срабатывает каждое правило, и ни одно не завышает свою уверенность.tests/test_cli.py— читатель и индексатор могут одновременно удерживать базу данных.tests/test_docs.py— сгенерированная демо-страница является корректным HTML со сбалансированными тегами; именно так был пойман баг перекрёстных тегов в Markdown-рендерере наmin_confidence.
Ограничения
Говоря прямо, инструмент анализа кода, который завышает свою точность, хуже бесполезного:
Никакого вывода типов.
self.conn.execute(...)нельзя сопоставить с символом репозитория, не зная типconn. Такие случаи попадают вunresolved, и они составляют основную часть остатка при ~85% внутреннего разрешения.Динамическая диспетчеризация невидима.
getattr(obj, name)(), реестры декораторов и DI-контейнеры не отображаются как рёбра.Межъязыковые рёбра не отслеживаются. TypeScript-фронтенд, вызывающий Python-эндпоинт, — это два разобщённых подграфа.
Только определения, а не все ссылки. Символ, используемый как значение (переданный как колбэк), слабее в графе, чем тот, который вызывается.
Дорожная карта: адаптеры для Rust и Java, опциональное обогащение через LSP для точного разрешения там, где доступен языковой сервер, и режим --changed-since <ref> для радиуса поражения в рамках PR.
Зачем это существует
Я хотел выяснить, можно ли главную слабость кодинг-агента на больших репозиториях — отсутствие структурной модели кода — исправить с помощью статического анализа и хорошо продуманной поверхности инструмента, а не более крупной модели или векторной базы данных. В основном — да.
Лицензия
MIT
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Connectors
Give your AI agent a persistent map of your project's structure, dependencies, and bugs.
Driflyte MCP server which lets AI assistants query topic-specific knowledge from web and GitHub.
AI Agent with Architectural Memory. Impact analysis (free), tests and code from the graph (pro).
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/GokulRaj2210/cartograph-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server