yasinyaman

graphlore

Community yasinyaman
Updated

MCP server that lets AI assistants explore a codebase through its knowledge graph (Graphify): natural-language locate with semantic search + hidden links, token-budgeted subgraphs, impact/blast radius, structural diff & freshness, duplication scan β€” 27 tools, multi-language, keyless by default.

graphlore

CILicense: MITPython 3.10+

A Python MCP server that exposes the Graphify knowledge graph as MCP tools, prompts and resources β€” so an AI assistant can explore your codebase through the graph during development, cheaply (token-budgeted) and structurally.

Note: Graphify ships its own embedded MCP server (graphify ./raw --mcp). This project adds analysis tools, token-budgeted subgraph extraction, git freshness checks, per-community resources, reusable prompts, and LLM-friendly tool annotations + structured (JSON) output on top.

Why graphify_locate

One MCP call turns a natural-language question into a navigational map, not a wall of code:

  • πŸ”Ž Semantic + structural, one call β€” semble finds the relevant code, the graph gives its neighborhood. ~235 tokens to orient vs ~61k for grep+read (263Γ— fewer on httpx).
  • πŸ”— hidden_links β€” semantically similar code that is structurally disconnected (duplication / missing-abstraction / sync-async-twin candidates) that neither search nor the graph surfaces alone.
  • 🌍 Multi-language, zero config β€” Python via stdlib ast; JS/TS Β· Go Β· Java Β· Rust Β· C++ Β· 165+ more via tree-sitter with automatic language detection. Span-join precision 70–96% on real HTTP-client repos in six languages (benchmark).
  • πŸ•’ Cosmetic-aware freshness β€” graphify_freshness ignores comment/format-only edits (in every language) so a reformat never triggers a needless rebuild.

One call beats running semble and graphify separately

semble finds what's relevant; graphify gives how it connects. They're complementary β€” but stitching them by hand means four calls, ~2.7k tokens, and manually aligning semble's line ranges to graph nodes. graphlore does that join for you, in one call:

per query semble alone graphify alone both, by hand graphify_locate
Semantic search βœ“ β€” βœ“ βœ“
Graph structure β€” βœ“ βœ“ βœ“
Chunk β†’ symbol join β€” β€” you wire it βœ“ automatic
hidden_links cross-check β€” β€” β€” βœ“ only here
Calls 1 1 4 1
Tokens to orient 1,613 1,107 2,721 235

β†’ 11.6Γ— fewer tokens than running the two separately β€” in a single call, and hidden_links (semantically similar code that is structurally disconnected) is a signal neither tool produces alone. So the combined tool isn't just convenience: it's cheaper, and it surfaces something the parts can't. (full benchmark ↓)

Installation

# graphlore itself
pip install graphlore

# plus the Graphify CLI it wraps (needed for build/query/path/explain/add)
pip install graphifyy && graphify install

From source:

git clone https://github.com/yasinyaman/graphlore
cd graphlore
pip install -e ".[dev]"

Running

GRAPHIFY_PROJECT_DIR=/path/to/repo graphlore
# equivalently:
GRAPHIFY_PROJECT_DIR=/path/to/repo python -m graphlore

Renamed from graphify-mcp: the old name collided with thegraphify-mcp console script that graphifyy ships for its embeddedserver, which forced the clunky graphify-mcp-server entry point. Asgraphlore the bare command is ours. The boot banner on stderr(graphlore vX.Y.Z | transport=… | project=…) confirms which serverand project dir you're actually running.

Claude Code

Copy mcp.json to a .mcp.json at your project root. GRAPHIFY_PROJECT_DIR: "." uses the project root.

Claude Desktop / Cowork

Add the contents of claude_desktop_config.json to your Claude Desktop config:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json

Transport (stdio default, optional HTTP)

stdio is the default and the right choice for a per-developer local server. Toserve over HTTP instead (e.g. a shared graph for a team or a web MCP client):

GRAPHIFY_TRANSPORT=streamable-http GRAPHIFY_HOST=127.0.0.1 GRAPHIFY_PORT=8000 \
  GRAPHIFY_PROJECT_DIR=/path/to/repo graphlore

Any HTTP transport force-enables path containment (GRAPHIFY_RESTRICT_PATHS)so a network client can't drive graphify_build to extract arbitrary filesystempaths. HTTP binds 127.0.0.1 by default. To expose it beyond localhost, setGRAPHIFY_API_KEY β€” every request must then send Authorization: Bearer <key>(constant-time checked, 401 otherwise); binding a non-loopback host without a keyprints a warning.

When bound to a loopback host, the MCP SDK auto-enables DNS-rebindingprotection: only Host: 127.0.0.1 / localhost / ::1 requests are accepted. Areverse proxy in front (nginx/caddy on a public name forwarding to127.0.0.1) must therefore rewrite the Host header β€” or setGRAPHIFY_ALLOWED_HOSTS to the public name(s) (comma-separated, :* portwildcards allowed; * disables the protection for a trusted proxy).

The CLI is always invoked as an argument list with no shell (subprocess.runwith shell=False), so a build path or query string can't inject shell commands.For a shared/network deployment, also consider lowering GRAPHIFY_TIMEOUT (default600s) so a single slow graphify_build can't tie up a worker for ten minutes.

GRAPHIFY_TRANSPORT=streamable-http GRAPHIFY_HOST=0.0.0.0 GRAPHIFY_API_KEY=$(openssl rand -hex 16) \
  GRAPHIFY_PROJECT_DIR=/path/to/repo graphlore

For a smaller tool surface (helps some models pick the right tool), setGRAPHIFY_TOOLSET=lean to expose only the core exploration tools.

Environment variables

Variable Default Description
GRAPHIFY_PROJECT_DIR . Project root to extract the graph from
GRAPHIFY_OUT_DIR graphify-out Output folder name
GRAPHIFY_BIN graphify CLI path
GRAPHIFY_TIMEOUT 600 CLI timeout (seconds)
GRAPHIFY_RESTRICT_PATHS 0 Confine graphify_build's path to the project dir (auto-on for HTTP)
GRAPHIFY_TRANSPORT stdio stdio | streamable-http | sse
GRAPHIFY_HOST 127.0.0.1 Bind host for HTTP transports
GRAPHIFY_PORT 8000 Bind port for HTTP transports
GRAPHIFY_API_KEY (unset) Require Authorization: Bearer <key> on HTTP transports
GRAPHIFY_ALLOWED_HOSTS (unset) DNS-rebinding Host allowlist for HTTP (comma-separated, :* port wildcards; * disables). Unset = SDK default: loopback-only when bound to loopback
GRAPHIFY_TOOLSET full full | lean (core exploration tools only)
GRAPHIFY_TOKENIZER (heuristic) tiktoken β†’ exact token counts (needs the [tiktoken] extra); else chars/3.5 estimate
GRAPHIFY_SEMANTIC_BACKEND semble Semantic backend for locate/duplication_scan. semble (offline default) or a module.path:Factory implementing the SemanticIndex protocol (search/find_related; results expose .chunk.file_path/.start_line/.end_line) β€” plug in local sentence-transformers, an OpenAI-compatible / on-prem vLLM endpoint, etc.
GRAPHIFY_WATCH 0 1 β†’ watch the project for structural source changes and re-sync the graph automatically (needs the [watch] extra; cosmetic edits are ignored)
GRAPHIFY_WATCH_DEBOUNCE 2.0 Seconds to coalesce a burst of file events before re-graphing (watch mode)

Keeping the graph fresh

The analysis tools surface staleness for you: graphify_overview andgraphify_subgraph carry a lightweight graph_age ("built 3 commits ago"), andgraphify_freshness gives a full recommended_action (fresh / update / rebuild).To stop thinking about it, regenerate on every commit with a git post-commithook β€” the recommended first-class auto-update flow:

# .git/hooks/post-commit   (then: chmod +x .git/hooks/post-commit)
#!/bin/sh
# incremental, viz-free, backgrounded so the commit returns immediately
graphify . --update --no-viz >/dev/null 2>&1 &

Incremental --update only re-extracts changed files β€” it can't drop nodes fordeleted/renamed code on its own. graphify_prune closes that gap: it surgicallyremoves the phantom nodes (and their edges) for source files that are gone from theworking tree, so after a delete/rename you can graphify_prune (preview withdry_run=True) + graphify_build(update=True) instead of a full rebuild.graphify_freshness knows about this β€” it only steers to a rebuild while phantomnodes for the removed files still linger, and reports them in phantom_files. Anagent can also just call graphify_build(update=True) when graph_age /graphify_freshness says the graph drifted.

Tools

CLI-backed (the first two write state; the rest are read-only):

Tool Purpose
graphify_build Build/update the graph (--update, --cluster-only, --mode deep)
graphify_add Add a source by URL (arXiv, tweet)
graphify_query Natural-language query (--dfs, --budget)
graphify_path Exact path between two nodes
graphify_explain Everything about a node

graph.json analysis (read-only, no CLI needed, as_json=True for structured output):

Tool Purpose
graphify_overview Call first β€” size, god nodes, communities, surprises, suggested next steps
graphify_god_nodes Most connected nodes
graphify_communities Leiden community summaries
graphify_surprises Unexpected cross-domain connections
graphify_search Node search
graphify_neighbors 1-hop neighbors of a node
graphify_subgraph Token-budgeted BFS subgraph around a node β€” the cheap way to feed the model just the relevant slice
graphify_impact Reverse-dependency / blast radius β€” what breaks if a node changes (direction=dependents/dependencies/both), ordered by hop distance
graphify_node_details Node metadata: type, source file/line, docstring, community
graphify_skeleton def/class signatures (decorators kept, bodies stripped) for a file/node/community β€” the middle layer between the map and full code
graphify_fetch Token-budgeted source hydration — reads the real code for a node (its enclosing def/class span ± context), the map→code other half of subgraph/locate
graphify_freshness Is the graph stale vs. git HEAD? Returns recommended_action (fresh/update/rebuild) + reason β€” lingering phantom nodes / large changes steer to a rebuild
graphify_diff Structural changeset between two git refs (default HEAD~1..HEAD) β€” added/removed/renamed/modified, with cosmetic-only changes separated (file-level, for review/audit)
graphify_prune Drop phantom nodes (and their edges) for deleted/renamed source files β€” the surgical alternative to a full rebuild (dry_run=True to preview)
graphify_validate Lint the graph for dangling/duplicate/self-loop edges and orphan nodes (read-only)
graphify_duplication_scan Repo-wide hidden-link / duplication audit β€” the batch form of locate's hidden_links (similar-but-structurally-far pairs); needs [semble], outside lean
graphify_cycles Circular dependencies β€” strongly-connected node groups in the directed graph (an architectural smell), self-loops listed separately
graphify_package_apis Symbol-level external API surface β€” which names each external package is actually used for (fastapi: Depends, APIRouter), with qualified paths (numpy.linalg.norm) for version-diff audits; a lower bound (dynamic/star/getattr use is invisible). Python via stdlib ast; JS/TS, Go, Java need [treesitter]

Semantic naming (uses the host model via MCP sampling β€” no API key β€” or a backend key):

Tool Purpose
graphify_sampling_status Capability test: reports whether the client supports host-LLM sampling, whether a backend key is set, and which method will be used
graphify_label_communities Give Leiden communities human-readable names. method="auto" (sampling β†’ key β†’ placeholder), "sampling", "cli", or "placeholder"
graphify_set_labels Persist assistant-provided community names (sampling-free fallback) to .graphify_labels.json and patch them into graph.html

Semantic bridge (optional [semble] extra β€” semantic search joined to graph structure):

Tool Purpose
graphify_locate NL query β†’ enclosing graph node β†’ token-budgeted subgraph, plus hidden_links: semantically-similar code that is structurally disconnected (duplication / missing-abstraction candidates)

Naming communities without an API key (MCP sampling)

The Leiden clustering is keyless, but turning Community 7 into Authenticationneeds a model. Three ways, in graphify_label_communities's preference order:

  1. Host-LLM sampling β€” the server asks the connected client to run thecompletion via MCP sampling/createMessage. The model the user already uses(e.g. Claude in a sampling-capable client) does the naming; the server holdsno API key. Subject to client support β€” call graphify_sampling_statusfirst; it degrades gracefully when unsupported. All communities are named ina single batched request, carried over whichever transport the negotiatedprotocol allows (the legacy back-channel, or input-required rounds on MCP2026-07-28+), so it works with both older and modern clients.
  2. Backend API key (method="cli") β€” set GEMINI_API_KEY / OPENAI_API_KEY/ ANTHROPIC_API_KEY / … (or run a local ollama) and graphify's ownbackend names them. This option always remains available.
  3. Placeholders β€” no model anywhere: names stay Community N.

If the client can't sample and you have no backend (e.g. Claude Code, whichdoesn't support sampling), use the assistant-driven fallback: the assistantis already a capable model in the loop, so it reads graphify_communities andpushes names back via graphify_set_labels({"0": "Authentication", ...}) β€”no key, no sampling, works in any client. The names persist to.graphify_labels.json and are patched into graph.html.

Semantic bridge (optional [semble])

pip install "graphlore[semble]" adds graphify_locate, which joinssemble's semantic code search to the graphin one call. Graphify gives structure (how code connects); semble givesretrieval (which code is semantically relevant) β€” they're complementary.

graphify_locate("how does retry backoff work"):

  1. semble finds the most relevant code and resolves the top hit to its enclosinggraph node (better than label matching).
  2. returns the token-budgeted subgraph around it (structure).
  3. runs semble find_related and cross-checks: a cousin that is semanticallysimilar but not within the seed's structural neighborhood is flagged as ahidden_link (with its hop distance) β€” a duplication / missing-abstraction /implicit-coupling candidate that neither tool surfaces alone.

The extra is optional: without it the core tools are unchanged and graphify_locatereturns an install hint. It also pairs well with running semble's own MCP serveralongside graphlore.

The chunkβ†’node join and the freshness cosmetic-vs-structural check workacross languages: Python uses the stdlib ast (no extra deps), and everyother language (JS/TS, Go, Rust, Java, Ruby, C/C++, …) is handled by an optionaltree-sitter backend β€” pip install "graphlore[treesitter]", also pulled inby graphify. Without it, non-Python files fall back to nearest-line matching.

Benchmark

Averaged over 6 queries spanning httpx subsystems (send path, digest auth,redirects, content decoding, cookies, timeouts) on the 2,101-node graph. Each queryorients an agent to a code area; tokens = what reaches the model's context(β‰ˆ chars/4).

Tokens to orient an agent across 6 httpx queries β€” lower is better

Approach Tokens (avg) Calls Structure Semantic Hidden links
Naive grep + read 61,836 ~14 β€” β€” 0
semble alone 1,613 1 β€” βœ“ 0
graphify alone 1,107 1 βœ“ β€” 0
semble + graphify (separately) 2,721 4 βœ“ βœ“ 0
graphify_locate 235 1 βœ“ βœ“ 7

graphify_locate averages 263Γ— fewer tokens than grep+read and 11.6Γ— fewerthan running semble and graphify separately (one call instead of four) β€” and it'sthe only approach that surfaces hidden_links (semantically similar but structurallydisconnected code), 5–10 per query.

Those ~235 tokens are a navigational map (seed file:line + structuralneighborhood + hidden links), not raw code β€” you fetch the specific code only whereneeded. That's the trade graphlore optimizes: cheapest orientation plus thecross-check signal, then drill in precisely.

Case study β€” the hidden links are real. Asked "does httpx duplicaterequest-sending across sync and async?", graphify_locate returned the seedClient._send_single_request and flagged hidden links. Checking the sourceconfirmed every production flag is a genuine sync/async twin:Client._send_single_request (_client.py:1001) ↔ AsyncClient._send_single_request(:1717); BaseTransport.handle_request ↔ handle_async_request (in everytransport); __enter__ ↔ __aenter__. ~500 tokens (one locate + a targeted read)surfaced a real architectural pattern that naively reading _client.py (~16k tokens)would. The unreachable bucket also held test files (related, not refactor targets) β€”the distance field separates production parallels (3–4) from that noise.

Across languages β€” real HTTP-client repos. The span join and freshness check aren'tPython-only. I built AST-only graphs for an HTTP client in five more languages and ran thesame kind of queries (send Β· redirects Β· timeout/retry Β· headers/auth Β· transport):

Span-join precision across languages β€” Python 96%, Go 93%, JS/TS 89%, Java 85%

Language Repo Span-join precision Qualname Hidden / q locate vs grep
Python (ast) encode/httpx 96% (52/54) 67% 3.2 272Γ—
JavaScript / TS sindresorhus/got 89% (48/54) 67% 2.3 583Γ—
Go go-resty/resty 93% (50/54) 100% 1.8 911Γ—
Java square/retrofit 85% (46/54) 50% 2.3 217Γ—
Rust algesten/ureq 70% (38/54) 83% 3.7 577Γ—
C++ libcpr/cpr 72% (39/54) 100% 4.3 195Γ—

Python uses the stdlib ast; JS/TS Β· Go Β· Java Β· Rust Β· C++ go through tree-sitter withautomatic language detection β€” one tool, zero per-language config. Span-join precision =share of semantic hits landing inside the resolved symbol's real span (any overload of it β€”C++ collapses same-name overloads into one graph node while each keeps its own span; cpr'sSession::SetOption has 46). It's 70–96% across six 350–2,095-node graphs, hidden-linkskeep surfacing 2–4/query, and locate stays 195–911Γ— cheaper than grep+read. Rust and C++trail at 70–72% β€” their misses are mostly file-top/whole-file chunks and namespace-level freefunctions where the resolution is still correct (they recover qualified names at 83–100%).graphify_freshness's cosmetic-vs-structural check is correct in every language too(comment/reformat β†’ cosmetic; operator/rename β†’ structural). Re-measured 2026-08 on the MCP v2SDK, Python 3.14, fresh repo HEADs. Reproduce withbenchmarks/multilang.py.

β†’ Full benchmark report (interactive HTML, per-query breakdown + the cross-language tables) β€” or open docs/benchmark.html locally. (TΓΌrkΓ§e)

Measured 2026-06 with semble 0.3.4 + graphify (tree-sitter backend). httpx headline = 6queries (per-query locate 189–286 tokens); cross-language = 6 queries Γ— 54 hits each ongot / resty / retrofit / ureq / cpr. Sample bias: every repo benchmarked here isan HTTP-client library β€” a deliberately uniform family chosen for cross-language comparability.Token savings and span-join precision will differ on other architectures (data pipelines, GUIapps, sprawling monorepos), so treat these as indicative, not guarantees. Numbers vary bycodebase and query.

Resources

  • graphify://report β€” GRAPH_REPORT.md
  • graphify://graph β€” graph.json (raw)
  • graphify://community/{id} β€” per-community wiki (members + internal/boundary edges)

Prompts

Reusable templates that orchestrate the tools for the assistant:

  • onboard β€” orient to the codebase (overview β†’ communities β†’ subgraphs β†’ surprises β†’ summary)
  • trace_bug(symptom) β€” find likely root-cause locations through the graph
  • explain_flow(flow) β€” end-to-end walkthrough of a named flow with file:line refs

LLM-friendliness

  • Tool annotations (read_only_hint, destructive_hint, titles) tell the model which tools are safe to call freely vs. which mutate state.
  • Server instructions describe the recommended flow (overview β†’ targeted subgraph/query β†’ build update).
  • as_json output on every analysis tool returns structured data the model can chain on instead of re-parsing prose.
  • Token budgeting (graphify_subgraph) keeps context small on large graphs β€” the core of Graphify's ~71Γ— compression.
  • Host-LLM sampling (graphify_label_communities) lets the server borrow the client's model via MCP sampling/createMessage, so semantic naming works with no server-side API key β€” with a capability test (graphify_sampling_status) and a backend-key fallback.

Typical workflow

  1. graphify_overview() β€” orientation
  2. graphify_communities() β€” subsystems
  3. graphify_subgraph("SomeNode") β€” token-cheap targeted exploration
  4. graphify_query("how does the auth flow work?") β€” questions
  5. After code changes: graphify_freshness() β†’ graphify_build(".", update=True)

Project layout

graphlore/
β”œβ”€β”€ src/graphlore/      # package (server.py, __init__.py)
β”œβ”€β”€ tests/                 # pytest suite + fixture graph.json
β”œβ”€β”€ .github/workflows/     # CI (ruff + pytest, py 3.10–3.12)
β”œβ”€β”€ pyproject.toml         # packaging + console script
β”œβ”€β”€ mcp.json               # Claude Code example config
└── claude_desktop_config.json

Development

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

See CONTRIBUTING.md. Licensed under MIT.

MCP Server Β· Populars

MCP Server Β· New

    ocm-mcp-server

    πŸ›‘οΈ ocm-mcp-server

    An MCP server that lets AI agents operate a multi-cluster Kubernetes fleet through an Open Cluster Management hub, with policy, approval, and audit between the model and your clusters.

    Community ocm-mcp-server
    M4F-S

    Gomaa 🧠

    Gomaa β€” Autonomous Agent Memory OS. Persistent memory system for AI agents with Obsidian vault integration, hybrid RRF search, knowledge graphs, security gates, and MCP server.

    Community M4F-S
    chatmcp

    3802

    directory for Awesome MCP Servers

    Community chatmcp
    Morningstar202604

    AgentSeed

    Anti-hallucination gate for AI coding agents β€” 8 MCP tools catch invented APIs (17 languages), fake "all tests pass" claims, and slopsquatting packages before they ship. Zero-dependency Agent Plugins 1.0.0 plugin (Skill + MCP server + CLI + CI gate) for Claude Code, Cursor, VS Code, Copilot.

    Community Morningstar202604
    skarn-security

    Skarn guard: agent plugins

    Skarn plugins for Claude Code, Codex CLI, Gemini CLI, Grok Build, and Antigravity: audit skills, guard hooks, and MCP declarations that find leaked secrets and credentials in AI coding sessions, locally and redacted.

    Community skarn-security