Graphloom MCP Server
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Graphloom MCP Serverhow does create_invoice reach the invoices table? cite file:line"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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 |
| Versioned graph ( |
| Hubs, isolated modules, cross-layer links, unread config keys, and questions the graph can already answer. |
| 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 budgetgraphloom path <a> <b>— shortest path with evidence on each hopgraphloom explain <node>— one node, every edgegraphloom ask "question"— a small retrieved subgraph plus an LLM answer that cites node ids andfile:linegraphloom serve— MCP stdio server so Cursor or Claude Code can call the same tools
The offline build understands:
Python via the stdlib
astmodule: modules, classes, functions, imports, and callsJavaScript / TypeScript with a small scanner: imports, exported functions and classes, calls,
fetchURLsSQL
CREATE TABLEnodes, plus foreign keysMarkdown 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_receiptOpen .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:8The 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 |
| Read from syntax. An import binding, a call to a function defined in the same file, a |
| Resolved afterward. A backtick in a doc, a table name inside a SQL string, a unique function name with no import, a |
| 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 |
| empty | API key used when |
| empty | Wins over |
|
| OpenAI-compatible base URL. |
|
| Model id for |
|
|
|
.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 -qGitHub Actions runs ruff and pytest on Python 3.10 and 3.12.
License
MIT. See LICENSE.
This server cannot be deployed
Maintenance
Related MCP Connectors
Repository knowledge graph MCP server for codebase understanding and debugging.
Hosted code graph over MCP: exact callers, dependencies, and cross-repo blast radius for AI agents.
- WauldoOAuthcom.wauldo
Stateless agentic tools over MCP: concept extraction, long-context, knowledge graph, planning.
Search indexed code, trace dependencies, assess change impact, and recall repository memory.
Related MCP Servers
- AlicenseNot gradedqualityBmaintenanceEnables 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 npm7MIT
- AlicenseNot gradedqualityCmaintenanceEnables AI coding agents to query a codebase as a knowledge graph, providing token-budgeted context, search, and impact analysis via MCP tools.MIT
- FlicenseNot gradedqualityAmaintenanceEnables 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.-
- AlicenseNot gradedqualityBmaintenanceEnables 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 npmApache 2.0