Skip to main content
Glama

Cartograph

Агент-ориентированный интеллект для кода. Превратите любой репозиторий в запрашиваемый граф кода и отдайте его агентам через MCP — чтобы агент мог спросить: «что сломается, если я изменю это?», а не грепать и надеяться.

tree-sitter + SQLite. Без эмбеддингов, без векторного хранилища, без API-ключей, без сервера, без затрат.

→ Живое демо — создаётся из реального индекса этого репозитория при каждом пуше.

CI Python 3.11+ License MIT


Проблема

Дайте агенту программирования большой незнакомый репозиторий и посмотрите, что он делает: 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 вместо того, чтобы гадать.


Десять инструментов

Инструмент

Ответ

find_symbol

Где определён X? (ранжировано по структурной значимости)

search_code

Полнотекстовый поиск по именам, сигнатурам, докстрингам (BM25)

get_symbol

Один символ: сигнатура, документация, члены, вызывающие, вызываемые, исходный код

who_calls

Обратное дерево вызовов — перед изменением сигнатуры

what_it_calls

Прямое дерево вызовов — понять код, не читая каждый файл

blast_radius

Что может сломать изменение, и какие тесты запускать

related_symbols

«Что ещё почитать?» через персонализированный PageRank

file_summary

Что файл определяет, что импортирует и кто его импортирует

architecture_overview

Модули, слои, циклы импортов, горячие точки, точки входа

index_stats

Здоровье индекса и разбивка разрешения рёбер по правилам

Плюс MCP-ресурсы (cartograph://architecture, cartograph://stats) и промпт orient для первичного прохода по незнакомому репозиторию через граф.

Языки: Python, TypeScript, TSX, JavaScript, Go.


Проектные решения, о которых стоит поспорить

1. Уверенность — полноправная колонка

Без тайпчекера нельзя знать, что store.who_calls() означает GraphStore.who_calls. Можно лишь ранжировать гипотезы. Поэтому вместо притворства каждое ребро записывает правило, которое его породило, и уверенность:

Правило

Уверенность

Смысл

same-file

0.95

определение прямо здесь, в области видимости

import

0.90

файл явно импортировал это имя

receiver-type

0.85

Foo.bar(), где Foo — известный контейнер

same-module

0.75

соседний файл в том же пакете

unique-global

0.60

ровно один символ репозитория с таким именем, голый вызов

name-only

0.45

одно совпадение, но на нетипизированном приёмнике

ambiguous

≤0.40

N кандидатов, сохраняется как N рёбер по 1/N каждое

external

0.00

корень в импорте сторонней/стандартной библиотеки

unresolved

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

Символы

Рёбра

Холодный

Тёплый

БД

Внутреннее разрешение

django

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%

flask

83

18

1,624

4,271

0.21s

0.03s

1.7 MB

87.4%

Задержка запросов (медиана из 5, тёплый режим):

Репозиторий

find_symbol

who_calls d3

blast_radius

architecture_overview

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))

Модуль

Назначение

indexer/walker.py

Обнаружение файлов — использует git ls-files для корректной семантики .gitignore

indexer/languages.py

По одному адаптеру на язык: расширения, запросы, докстринги, ключи модулей, разрешение импортов

indexer/extract.py

AST → символы/ссылки/импорты, без привязки к языку

queries/*.scm

tree-sitter паттерны захвата — знания по каждому языку в виде данных

graph/schema.sql

Граф: files, symbols, refs, edges, imports, FTS5

graph/resolver.py

Каскад уверенности

graph/algorithms.py

PageRank, персонализированный PageRank, итеративный Tarjan SCC, слои

graph/store.py

Обход через рекурсивные CTE, ранжированный поиск, агрегаты

service.py

Единый фасад, чтобы CLI и MCP-сервер не расходились

views.py

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               # strict

CI запускает набор тестов на 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

-
license - not tested
-
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

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).

View all MCP Connectors

Latest Blog Posts

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