Skip to main content
Glama
bayuh85
by bayuh85

Graphloom

Coding agents answer architecture questions by grepping and reading file after file. They burn tokens and still miss how the repository fits together: which module calls which function, which doc names which symbol, which handler touches which table. Graphloom weaves that map once, offline, and lets an agent ask a short question against it.

What it does

graphloom build walks a repository and writes three files under .graphloom/:

File

Role

graph.json

Versioned graph (graphloom.graph/v1). Every edge has a provenance tag and a file:line excerpt.

REPORT.md

Hubs, isolated modules, cross-layer links, unread config keys, and questions the graph can already answer.

graph.html

One self-contained viewer. Search, filter, click a node. No CDN, no network.

You then query that file instead of the tree:

  • graphloom query <term> — matching nodes and their neighbors, trimmed to a token budget

  • graphloom path <a> <b> — shortest path with evidence on each hop

  • graphloom explain <node> — one node, every edge

  • graphloom ask "question" — a small retrieved subgraph plus an LLM answer that cites node ids and file:line

  • graphloom serve — MCP stdio server so Cursor or Claude Code can call the same tools

The offline build understands:

  • Python via the stdlib ast module: modules, classes, functions, imports, and calls

  • JavaScript / TypeScript with a small scanner: imports, exported functions and classes, calls, fetch URLs

  • SQL CREATE TABLE nodes, plus foreign keys

  • Markdown headings, with edges to symbols written in backticks

  • TOML / YAML / JSON keys, with edges from code that reads them

An optional --semantic pass sends documentation chunks, not source code, to a model and grounds the concepts it returns onto nodes that already exist.

Related MCP server: io.github.pmgarg/cgraphy

Quickstart

pip install -e .
cd examples/harbor-shop
graphloom build .
graphloom query invoices
graphloom path post_invoice send_receipt

Open .graphloom/graph.html in a browser. The sample project is Harbor Supply, a tiny wholesale checkout: Python services, a SQL ledger, a browser cart, and two docs. From another directory, pass --graph path/to/.graphloom/graph.json.

Real output from that example:

$ graphloom build .
graphloom build .
nodes: 67
edges: 108
  parsed: 75
  linked: 33
  llm: 0
wrote graph: .graphloom/graph.json
wrote report: .graphloom/REPORT.md
wrote html: .graphloom/graph.html
$ graphloom query invoices
query: invoices
matches: 10
showing: 6 nodes, depth 1
budget: 1600 tokens (~1494 used)
truncated: yes

## tbl:invoices
kind: table   layer: data   name: invoices
at: sql/schema.sql:17
columns: id, customer_id, sku, total_cents, status
edges:
  -> ties [parsed] tbl:customers (customers)  sql/schema.sql:17  CREATE TABLE invoices (
  -> ties [parsed] tbl:products (products)  sql/schema.sql:17  CREATE TABLE invoices (
  <- contains [parsed] sql:sql/schema.sql (sql/schema.sql)  sql/schema.sql:17  CREATE TABLE invoices (
  <- mentions [linked] doc:docs/billing.md:h:creating-an-invoice (Creating an invoice)  docs/billing.md:9  the `invoices` table. The in-memory draft is `InvoiceDraft`. The customer
  <- ties [parsed] tbl:payments (payments)  sql/schema.sql:25  CREATE TABLE payments (
  <- touches [linked] fn:backend/billing/invoices.py:create_invoice (create_invoice)  backend/billing/invoices.py:35  "INSERT INTO invoices (customer_id, sku, total_cents, status) "

## mod:backend/billing/invoices.py
kind: module   layer: code   name: backend.billing.invoices
at: backend/billing/invoices.py:1
edges:
  -> contains [parsed] cls:backend/billing/invoices.py:InvoiceDraft (InvoiceDraft)  backend/billing/invoices.py:7  class InvoiceDraft:
  -> contains [parsed] fn:backend/billing/invoices.py:_lookup (_lookup)  backend/billing/invoices.py:47  def _lookup(statement: str, email: str) -> int:
  -> contains [parsed] fn:backend/billing/invoices.py:_write_invoice (_write_invoice)  backend/billing/invoices.py:51  def _write_invoice(statement: str, row: tuple) -> None:
  -> contains [parsed] fn:backend/billing/invoices.py:create_invoice (create_invoice)  backend/billing/invoices.py:25  def create_invoice(payload: dict, settings: dict) -> dict:
  -> contains [parsed] fn:backend/billing/invoices.py:format_total (format_total)  backend/billing/invoices.py:19  def format_total(cents: int, settings: dict) -> str:
  -> imports [parsed] fn:backend/billing/payments.py:record_payment (record_payment)  backend/billing/invoices.py:3  from backend.billing.payments import record_payment
  -> imports [parsed] fn:backend/catalog/products.py:price_for (price_for)  backend/billing/invoices.py:4  from backend.catalog.products import price_for, reserve_stock
  -> imports [parsed] fn:backend/catalog/products.py:reserve_stock (reserve_stock)  backend/billing/invoices.py:4  from backend.catalog.products import price_for, reserve_stock

## tbl:customers
kind: table   layer: data   name: customers
at: sql/schema.sql:3
columns: id, email, name
edges:
  <- contains [parsed] sql:sql/schema.sql (sql/schema.sql)  sql/schema.sql:3  CREATE TABLE customers (
  <- mentions [linked] doc:docs/billing.md:h:creating-an-invoice (Creating an invoice)  docs/billing.md:10  check reads `customers`.
  <- ties [parsed] tbl:invoices (invoices)  sql/schema.sql:17  CREATE TABLE invoices (
  <- touches [linked] fn:backend/billing/invoices.py:create_invoice (create_invoice)  backend/billing/invoices.py:30  lookup = "SELECT id, email FROM customers WHERE email = ?"

## tbl:products
kind: table   layer: data   name: products
at: sql/schema.sql:9
columns: sku, name, price_cents, held, active
edges:
  <- contains [parsed] sql:sql/schema.sql (sql/schema.sql)  sql/schema.sql:9  CREATE TABLE products (
  <- mentions [linked] doc:docs/billing.md:h:catalog (Catalog)  docs/billing.md:20  Shelf prices come from the `products` table through `list_products`.
  <- ties [parsed] tbl:invoices (invoices)  sql/schema.sql:17  CREATE TABLE invoices (
  <- touches [linked] fn:backend/catalog/products.py:list_products (list_products)  backend/catalog/products.py:15  statement = "SELECT sku, name, price_cents FROM products WHERE active = 1"
  <- touches [linked] fn:backend/catalog/products.py:price_for (price_for)  backend/catalog/products.py:5  statement = "SELECT price_cents FROM products WHERE sku = ?"
  <- touches [linked] fn:backend/catalog/products.py:reserve_stock (reserve_stock)  backend/catalog/products.py:10  statement = "UPDATE products SET held = held + ? WHERE sku = ?"

## sql:sql/schema.sql
kind: sqlscript   layer: data   name: sql/schema.sql
at: sql/schema.sql:1
edges:
  -> contains [parsed] tbl:customers (customers)  sql/schema.sql:3  CREATE TABLE customers (
  -> contains [parsed] tbl:invoices (invoices)  sql/schema.sql:17  CREATE TABLE invoices (
  -> contains [parsed] tbl:payments (payments)  sql/schema.sql:25  CREATE TABLE payments (
  -> contains [parsed] tbl:products (products)  sql/schema.sql:9  CREATE TABLE products (
  <- mentions [linked] doc:README.md:h:harbor-supply (Harbor Supply)  README.md:8  Tables are declared in `sql/schema.sql`.
  <- mentions [linked] doc:docs/overview.md:h:where-the-rows-live (Where the rows live)  docs/overview.md:13  Tables are declared in `sql/schema.sql`. Mail settings live in

## doc:docs/billing.md:h:creating-an-invoice
kind: heading   layer: docs   name: Creating an invoice
at: docs/billing.md:5
edges:
  -> mentions [linked] cls:backend/billing/invoices.py:InvoiceDraft (InvoiceDraft)  docs/billing.md:9  the `invoices` table. The in-memory draft is `InvoiceDraft`. The customer
  -> mentions [linked] fn:backend/app.py:post_invoice (post_invoice)  docs/billing.md:7  `post_invoice` is the HTTP entry. It calls `create_invoice`, which prices the
  -> mentions [linked] fn:backend/billing/invoices.py:create_invoice (create_invoice)  docs/billing.md:7  `post_invoice` is the HTTP entry. It calls `create_invoice`, which prices the
  -> mentions [linked] fn:backend/catalog/products.py:price_for (price_for)  docs/billing.md:8  sku with `price_for`, holds stock with `reserve_stock`, and inserts a row in
  -> mentions [linked] fn:backend/catalog/products.py:reserve_stock (reserve_stock)  docs/billing.md:8  sku with `price_for`, holds stock with `reserve_stock`, and inserts a row in
  -> mentions [linked] tbl:customers (customers)  docs/billing.md:10  check reads `customers`.
  -> mentions [linked] tbl:invoices (invoices)  docs/billing.md:9  the `invoices` table. The in-memory draft is `InvoiceDraft`. The customer
$ graphloom path post_invoice send_receipt
path: post_invoice -> send_receipt
resolved: fn:backend/app.py:post_invoice -> fn:backend/notify/emailer.py:send_receipt
hops: 4
directed: yes

1. fn:backend/app.py:post_invoice
   function post_invoice
   backend/app.py:9
   invokes [parsed] -> fn:backend/billing/invoices.py:create_invoice
   backend/app.py:10  return create_invoice(payload, settings)

2. fn:backend/billing/invoices.py:create_invoice
   function create_invoice
   backend/billing/invoices.py:25
   invokes [parsed] -> fn:backend/billing/payments.py:record_payment
   backend/billing/invoices.py:39  record_payment(str(payload["invoice_id"]), total, "card", str(payload["email"]))

3. fn:backend/billing/payments.py:record_payment
   function record_payment
   backend/billing/payments.py:6
   invokes [parsed] -> fn:backend/notify/emailer.py:queue_receipt
   backend/billing/payments.py:12  queue_receipt(email, invoice_id)

4. fn:backend/notify/emailer.py:queue_receipt
   function queue_receipt
   backend/notify/emailer.py:4
   invokes [parsed] -> fn:backend/notify/emailer.py:send_receipt
   backend/notify/emailer.py:5  send_receipt(email, invoice_id)

5. fn:backend/notify/emailer.py:send_receipt
   function send_receipt
   backend/notify/emailer.py:8

The query stopped inside the token budget (truncated: yes): the invoices table is touched by create_invoice, tied to customers, products, and payments, and named from the billing doc. The path from the HTTP route to the mailer is four parsed calls.

graphloom ask and graphloom build --semantic need an API key. The build, the viewer, and query / path / explain do not.

export ATMOROUTER_API_KEY=ar-your-key-here
graphloom build . --semantic --max-calls 4
graphloom ask "what touches the invoices table?"

Provenance

Every edge is tagged so an agent can tell a fact the parser read from a guess.

Tag

Meaning

parsed

Read from syntax. An import binding, a call to a function defined in the same file, a CREATE TABLE / foreign key, or a literal settings["smtp"]["host"] path.

linked

Resolved afterward. A backtick in a doc, a table name inside a SQL string, a unique function name with no import, a fetch("/invoices") matched to a route.

llm

Added by the semantic pass. The model proposed a concept and an anchor; Graphloom kept the anchor only when it names a node that already exists.

Evidence is a list of {file, line, excerpt} on the edge. explain and path print it. The viewer shows it in the side panel.

Relations used in the graph: contains, imports, invokes, touches (code to a table), reads (code to a config key), mentions (a heading to a symbol), hits (a client fetch to a route), ties (a foreign key), about (a concept to a node).

AtmoRouter

The code graph is built free and offline. Only two things ever call a model: the optional semantic pass over documentation, and graphloom ask. Both send a short prompt (doc chunks, or a handful of nodes), not the repository. Routing those calls through a cheap AI API gateway keeps the bill small, which is why Graphloom defaults to AtmoRouter, an OpenRouter alternative with an OpenAI-compatible /v1/chat/completions API.

Set GRAPHLOOM_BASE_URL and GRAPHLOOM_API_KEY to point the same client at any other OpenAI-compatible endpoint. Set GRAPHLOOM_API_STYLE=anthropic to POST {base}/messages instead.

The default model is zai/glm-5.3-flash, a low-cost text model on the AtmoRouter catalog (GET https://atmorouter.dev/v1/models). Override GRAPHLOOM_MODEL if you want a different id.

Calls are batched, cached by a hash of the chunk, and stopped at --max-calls (default 8). A second semantic build of an unchanged doc does not call the model again.

Powered by AtmoRouter.

Configuration

Graphloom reads a .env file in the working directory if present, and does not override variables that are already set. Copy .env.example to start.

Variable

Default

Purpose

ATMOROUTER_API_KEY

empty

API key used when GRAPHLOOM_API_KEY is unset.

GRAPHLOOM_API_KEY

empty

Wins over ATMOROUTER_API_KEY.

GRAPHLOOM_BASE_URL

https://atmorouter.dev/v1

OpenAI-compatible base URL.

GRAPHLOOM_MODEL

zai/glm-5.3-flash

Model id for --semantic and ask.

GRAPHLOOM_API_STYLE

openai

openai posts to {base}/chat/completions. anthropic posts to {base}/messages.

.env.example contains ATMOROUTER_API_KEY=ar-your-key-here.

Cursor and Claude Code

Build the graph first (graphloom build .), then point the MCP server at it. The server speaks JSON-RPC over stdio (newline-delimited JSON, as the MCP spec requires; Content-Length framing is also accepted): initialize, notifications/initialized, tools/list, and tools/call. The tools are graph_query, graph_path, graph_explain, and graph_ask.

Cursor, .cursor/mcp.json:

{
  "mcpServers": {
    "graphloom": {
      "command": "graphloom",
      "args": ["serve", "--graph", ".graphloom/graph.json"]
    }
  }
}

Claude Code, .mcp.json in the project:

{
  "mcpServers": {
    "graphloom": {
      "command": "graphloom",
      "args": ["serve", "--graph", ".graphloom/graph.json"]
    }
  }
}

graph_ask uses the environment above. The other three tools stay offline.

Development

Requires Python 3.10+. Runtime dependencies are the standard library only (urllib for HTTP).

pip install -e ".[dev]"
ruff check .
pytest -q

GitHub Actions runs ruff and pytest on Python 3.10 and 3.12.

License

MIT. See LICENSE.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables AI agents to search code by meaning, explore codebase structure, store and query knowledge with temporal facts, and read source code through a set of MCP tools.
    203 npm
    7
    MIT
  • F
    license
    Not graded
    quality
    A
    maintenance
    Enables agents to build and query code knowledge graphs for repositories in a folder — finding shortest paths between concepts, explaining concepts with neighbours and community context, and visualizing per-repo graphs through MCP tools.
    -
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables AI coding agents to query a local, continuously updated symbol graph of a codebase, providing ranked search, caller/callee exploration, dependency paths, and git-diff impact analysis through MCP tools.
    1 npm
    Apache 2.0