Skip to main content
Glama

Cartograph

面向智能体的代码智能。 将任意仓库变成可查询的代码图,并通过 MCP 提供给编码智能体——这样智能体就可以问*“如果我改了这个,会破坏什么?”*,而不是靠 grep 和猜测。

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
…

一次调用,在编辑之前。而不是在测试套件变红之后再跑七次 grep。


快速开始

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.json 接入任意 MCP 客户端:

{
  "mcpServers": {
    "cartograph": {
      "command": "cartograph",
      "args": ["serve", "/path/to/repo"]
    }
  }
}

serve 在首次运行时(如果不存在索引)会进行索引。然后问你的智能体*“如果我改了 token 校验器,会破坏什么?”*,它会调用 blast_radius,而不是靠猜。


十个工具

工具

回答

find_symbol

X 在哪里定义?(按结构重要性排序)

search_code

对名称、签名、docstring 进行全文搜索(BM25)

get_symbol

单个符号:签名、文档、成员、调用方、被调用方、源码

who_calls

反向调用树——在修改签名之前

what_it_calls

正向调用树——无需阅读每个文件就能理解代码

blast_radius

改动可能破坏什么,以及应该运行哪些测试

related_symbols

“我还应该读什么?”通过个性化 PageRank

file_summary

文件定义了哪些内容、导入了什么,以及谁导入了它

architecture_overview

模块、分层、导入循环、热点、入口点

index_stats

索引健康度以及按规则划分的边解析明细

此外还有 MCP 资源(cartograph://architecturecartograph://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 这一层之所以存在,是因为一个真实的 bug。内置 set 上的 seen.add(...) 被解析到了仓库中某个类的 add 方法,仅仅因为这个名字恰好唯一——并且它显示为一个高置信度的调用方。无法确定类型的接收者上的方法名不能作为证据,所以它现在位于精确率线之下。(测试

external 的存在是为了让指标保持诚实:在大多数仓库中,“unresolved”桶主要由 typer.Optionsqlite3.execute 占据。如果把它们混在一起,会让覆盖率看起来比实际差得多,因此 Cartograph 报告内部解析率——在可能命中仓库符号的调用点中,实际命中的比例。

2. 解析是增量的;符号解析则从不增量

只有当文件的 sha256 变化时,才会重新解析。但原始引用会作为事实存储在 refs 表中,而 edges 会在任何内容发生变化时,作为(refs × symbols)的纯函数重新计算。

这正是让“每次编辑后重新索引”变得可信的原因。如果解析也是增量的,编辑一个文件可能会让另一个文件中的边指向一个已经移动的符号。全局重新解析在结构上杜绝了这种可能。(测试

这种代价是真实存在的,所以只有一个安全的捷径:如果没有文件被添加、重新解析或删除,那么两个输入表都不变,解析结果可证明完全相同——因此可以跳过。这让 Django 的无操作重新索引从 7.5 秒降至 0.67 秒,且生成的图逐字节一致。

3. 用 PageRank 代替嵌入

“你指的是哪个 get?”是一个结构性问题。四十个调用点所依赖的那个 get 才是智能体想要的,而调用图早就知道这一点。因此,符号排名是对调用图进行加权 PageRank——稳定、可解释、且零成本。无需模型、无需构建索引、无需向量存储。

related_symbols 扩展了同样的思路:以单个符号为种子的个性化 PageRank,将图视为无向图,因为当你即将修改一个函数时,它的调用方和被调用方都是相关的上下文。它是语义搜索的结构化对应物,并且不需要嵌入。

4. 工具返回 Markdown 而非 JSON,且在 token 预算内

消费者是上下文窗口。一个包含 40 个符号的 JSON 数组会在花括号和重复的键上浪费数千个 token,而且模型反正也会重新格式化。这里的每个视图都是带有硬性 token 预算的紧凑 Markdown。

关键是,每一次截断都会明确告知。如果一个智能体拿到 87 个调用方中的 20 个且没有任何标记,它会自信地断定其他 67 个不存在,然后删除某些东西。

5. 遍历在 SQLite 中运行,而非 Python

深度为 4 的 who_calls 是一个递归 CTE,因此整个遍历都停留在 SQLite 的 C 循环中。在 Django 拥有 252k 条边的图上,这大约需要 ~5ms。如果要把边表拉入 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

每种语言一个适配器:扩展名、查询、docstring、模块键、导入解析

indexer/extract.py

AST → 符号/引用/导入,与语言无关

queries/*.scm

tree-sitter 捕获模式——每种语言的知识,以数据形式存在

graph/schema.sql

图:filessymbolsrefsedgesimports、FTS5

graph/resolver.py

置信度级联

graph/algorithms.py

PageRank、个性化 PageRank、迭代 Tarjan 强连通分量、分层

graph/store.py

递归 CTE 遍历、排序查找、聚合

service.py

统一门面,确保 CLI 和 MCP 服务器不会漂移

views.py

受 token 预算约束的 Markdown

无需组合查询的作用域界定

queries/*.scm 保持小巧的诀窍:作用域从不编码在查询中。每个捕获的定义都按其 tree-sitter 节点 id 建立索引,而引用的所在符号则通过沿其 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)上运行测试套件,然后吃自己的狗粮:它为这个仓库建立索引,在出现导入循环时失败,断言无操作重新索引不会重新解析任何内容,并通过真实的 stdio 驱动 MCP 服务器。它还会将构建的 wheel 安装到干净的 venv 中并用它建立索引,因为打包的 .scm 文件很容易被遗漏在 wheel 之外,而且在本地几乎不可能注意到。

这个循环检查已经证明了它的价值——它捕获了我在这个仓库中引入的一个 store → resolver → store 循环,后来通过移动有问题的辅助函数而不是放宽检查来修复。

值得注意的测试

  • tests/test_queries.py — 每个 .scm 都能针对每一个加载它的语法进行编译,并捕获到一些内容。在 JavaScript 中有效的模式((class_heritage (identifier)))在 TypeScript 中是一个 不可能的模式,因为 TypeScript 将父类型包装在 extends_clause 中。那一行在 TypeScript 中静默地产生了零个符号。

  • tests/test_incremental.py — 在编辑、删除或符号在文件间移动后,不会留下陈旧的边。

  • tests/test_resolver.py — 每一条规则都会触发,且没有一条会过度宣称其置信度。

  • tests/test_cli.py — 读取器和索引器可以同时持有数据库。

  • tests/test_docs.py — 生成的演示页面是格式良好的 HTML,标签平衡,这正是 Markdown 渲染器在 min_confidence 上的交叉标签 bug 被发现的方式。


局限性

说得直白些:因为一款过度吹嘘其精度的代码智能工具比毫无用处更糟糕。

  • 不进行类型推断。 在不知道 conn 类型的情况下,self.conn.execute(...) 无法解析为仓库符号。这些会落入 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