site-kg
Site-kg turns a website or a local docs/code directory into a queryable knowledge graph served over MCP.
Ingest a site (
ingest_url) or a local directory of docs and code (ingest_local), with page/file limits,.gitignorehonoring, and optional derived relation dimensions (auto,llm,all,off, or a comma list); each ingest returns asite_idplus aREADY/NOT-READYgraph-quality verdict.Inventory everything ingested with
list_sites(verdicts and graph sizes) and inspect one site's health withsite_stats(doc/edge counts, edge types, top hubs).Search full text with an idf-weighted inverted index (
search), then pull the full markdown body and source URL of a hit (get_page).Expand context graph-wise:
neighborswalks a page's subgraph up to 2 hops, so an agent can gather multi-hop evidence around a search hit.Answer questions with
ask— keyword retrieval plus graph-neighborhood expansion, embedding rerank, and a cited answer (requires an OpenAI-compatible LLM gateway configured on the server; everything else works keyless).Inspect visually (human path):
render_siteemits a self-contained 3D Three.js viewer in archive mode or a live SSE-replayed session, andrender_buildreplays the graph's construction edge-by-edge in a WebGPU viewer.
In short: crawl or walk a corpus, keep only honest link/import edges (never invented ones), and expose it to an agent as search, page reads, graph traversal, and optional cited Q&A.
Enables crawling and ingesting Docusaurus documentation sites through sitemap.xml discovery, including sitemap index flattening, to build a searchable knowledge graph.
Enables crawling and ingesting Doxygen API reference sites via navtreeindex0.js manifest discovery, supporting search, page retrieval, and graph traversal across header and source pages.
Enables crawling and ingesting Sphinx documentation sites by enumerating pages from objects.inv, then building a searchable knowledge graph with page retrieval and neighbor traversal.
Enables crawling and ingesting VitePress documentation sites via sitemap.xml, supporting full-text search, page access, and graph-based context expansion.
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., "@site-kg crawl https://docs.example.com and search for authentication setup"
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.
site-kg
Give it a website URL or a local directory; it builds a knowledge graph from docs and code, and serves it to AI agents over MCP.
中文文档:README.zh-CN.md
What it does
URL ──► manifest probe ──► crawl ──► markdown corpus ──► graph.json + search.json ──► MCP server
(objects.inv → (httpx+bs4+html2text, (link-derived edges, (search / get_page /
sitemap.xml → BFS) robots.txt, rate-limited) inverted index, verdict) neighbors / site_stats)
local dir ──► walk (.gitignore-aware) ──► same corpus format ──► same graph + MCP
(md/txt/wiki docs, code files;
edges: md links, [[wikilinks]], imports, README hubs)Manifest probe — Sphinx sites are enumerated from
objects.inv(the authoritative page list), Doxygen sites fromnavtreeindex0.js(theirindex.htmlis a JS shell with no static links), thensitemap.xml, then same-site BFS. No guessing from local filenames.Edges come from the corpus only. If a site has no cross-references the verdict is
NOT-READYand the graph layer says so instead of drawing a decorative hairball. Never invents links.No LLM required for the structural layer: search, page bodies and graph traversal all work keyless. The optional
askQ&A layer runs on any OpenAI-compatible gateway (SITE_KG_LLM_BASE+SITE_KG_LLM_KEY).Wheels, not rewrites: MCP via the official
mcpSDK; optional JS-heavy crawling via Crawl4AI; optional semantic layer via cognee. Only the glue (manifest → corpus → graph → MCP) is ours.
Related MCP server: read-website-fast
Quickstart
uv venv && source .venv/bin/activate
uv pip install -e ".[crawl]"
# ingest a site (bounded)
python -m site_kg.cli ingest https://example.org/docs/index.html --max-pages 200
# or ingest a local directory (docs wiki + code repo)
python -m site_kg.cli ingest-local /path/to/repo-or-wiki
# serve MCP over streamable HTTP (for network clients)
python -m site_kg.cli serve --transport http --port 8766
# or stdio, for agent hosts that launch subprocesses
python -m site_kg.cli serveMCP client config (streamable HTTP):
{"mcpServers": {"site-kg": {"url": "http://127.0.0.1:8766/mcp"}}}
> The HTTP transport has no authentication: bind 127.0.0.1 (the default). To expose the server beyond loopback you must set `SITE_KG_ALLOW_REMOTE=1` for the server process, accepting the risk.MCP tools
tool | purpose |
| crawl + build graph; returns |
| local directory (docs + code) → graph; same outputs |
| ingested sites with verdicts |
| doc/edge counts, edge types, top hubs |
| idf-weighted full-text search |
| full markdown body + source URL |
| subgraph expansion (multi-hop context) |
| LLM Q&A with citations (needs LLM config) |
| self-contained 3D Three.js graph viewer (human inspection): degree-sized glowing nodes, hover tooltip, click-to-focus neighbourhood, chapters aggregate view (click a sphere to isolate), instant search with focus, live status bar (nodes/edges/view%/FPS) |
| self-contained WebGPU build-replay viewer: the graph constructs itself live — nodes born in corpus order onto 3D chapter shells, then evidence edges (ref/imports/contains), then one flying wave per derived relation dimension; every edge shoots out of its source node and binds to the target with a pulse while a HUD counts nodes/edges/per-dimension edges. Speed controls 0.25×–16×, replay/pause/skip. Needs a WebGPU-capable browser |
| the claim ledger, filtered — |
| stage one claim; it lands as |
| the site's lenses: canon pointer, each branch's state, and its signature recomputed (never read from a cache) |
| fork a branch as an overlay — it stores only the difference from the ledger, so claims adopted later are still visible through it |
| three sets between two lenses ( |
| open a dispute over claims already linked by a |
Resource: site://<site_id>/graph — the full graph JSON.
The claim layer is read and stage only on this surface. kg_claim_propose stages a claim as draft; nothing here can promote or retract one, because adoption carries approved_by and a tool parameter is a channel the caller controls — so the agent could sign its own proposal. Adoption is the CLI's (site-kg claim-decide). A claim staged through MCP always records the calling agent as its producer, and an agent name starting with human: is refused: that field is what the self-review check reads.
Local directories (docs + code)
ingest_local walks a directory into the same corpus format the crawler produces, so every downstream tool (search/get_page/neighbors/ask/render) works unchanged.
Docs (
.md/.markdown/.mdx/.txt/.wiki/.rst/.adoc) keep their body. Edges come from relative markdown links (extension-less and root-absolute forms included) and[[wikilinks]](Obsidian/MediaWiki style, case/underscore-insensitive basename match). Unresolvable references are counted indropped_refs, never turned into edges.Code (37 extensions: py, js/ts, go, rust, c/c++, java/kotlin, ruby, … plus the kernel/build ecosystem — yaml, asm
.S/.s, perl, awk, linker scripts, makefile fragments, devicetree.dts/.dtsi) becomes searchable docs whose body starts with a prose header (path, language, detected top-level symbols) so identifiers hit the inverted index. Well-known extension-less build files (Makefile,Kconfig,Kbuild,COPYING,LICENSE,README,MAINTAINERS, …) are ingested by basename with the same treatment. Edges come from real dependency statements: Pythonimport/from…import(including relative and parenthesized forms), JS/TSimport/require, C/C++ quoted#include, Goimport(go.mod-aware), Rustmod/use crate::, JVMimport, Rubyrequire_relative.README/index files are hubs:
containsedges to their directory siblings, mirroring the site-side hub semantics..gitignoreis honored (viapathspec) plus a builtin noise list (node_modules,__pycache__, lockfiles, *.min.js, …); >512 KB and binary files are skipped. Every skip is attributed, not silent:scan_statscountsskipped_noise_file/skipped_ext/skipped_oserrorseparately (their sum stays inskipped_kind), and NOISE_DIRS pruning reportspruned_noise_dirsplus a sample of the pruned paths — a coverage claim you cannot audit is a wrong claim.Page bodies are sharded, not monolithic:
data/sites/<site_id>/docs/<bucket>.jsonholds 1–256 sha-keyed buckets (count scaled to corpus size, recorded asdocBuckets), soget_pagereads one small bucket whatever the corpus shape (measured on a 75k-doc kernel tree: reading the single 975 MBdocs.jsontook 8.3 s, a sharded cold page read takes ~12 ms; adrivers/chapter with 31k files cannot recreate one giant shard). Sites built before sharding keep theirdocs.jsonand are still read transparently. Re-ingesting replaces the shards and drops the old single file in the same pass.
Env knobs (read per call, so a long-lived MCP server picks up changes without a restart):
var | effect |
| per-file size cap in bytes (default 512 KiB) |
| comma-separated posix relpath prefixes whose dirs escape NOISE_DIRS pruning (e.g. |
| derived relation dimensions, selected per node: |
| LLM entity-extraction budget per ingest (default 120) |
| embedding-similarity budget per ingest (default 300) |
| LLM router budget per ingest (default 120) |
PageRank runs over the edge list (aider repo-map style) and lands on each node as
r, sosite_stats/the viewer can rank structurally central files.
Design choices, after surveying the field (aider, Serena, CocoIndex, gitingest; GraphRAG, LightRAG, cognee, khoj; GraphCoder/CGM papers): the structural layer stays LLM-free and honest — every edge traces to an explicit link or import statement. LLM-extracted entity/relation edges (the GraphRAG/LightRAG approach) buy recall at the cost of hallucinated edges and build cost, so they stay in the optional cognee semantic layer. Compiler-grade precision (LSP/SCIP, Serena-style) and tree-sitter symbol graphs are roadmap items as optional extras, not hard dependencies.
Measured (2026-10-09, dogfooding this repo): 22 files (7 docs + 15 code) → 30 imports + 2 ref + 2 contains edges, verdict READY; identifier search (pagerank, gitignore) lands on the right source file.
Measured at corpus scale (2026-10-13, a 115,598-doc / 7-site DriveOS SDK source tree): re-ingest and report parity verified site-by-site; get_page on the largest site (75,415 docs) reads one ~3.8 MB shard in ~12 ms where the single-file format needed 8.3 s for the same page. Shard/meta caches are keyed by file mtime+size, so a re-ingest from any process (cli, crawl job, another agent) is served fresh with no invalidation hook; all writes are tmp-file + os.replace, and a 4-thread read hammer during repeated re-ingest saw zero torn reads.
Live viewer
site-kg live starts a local, address-first live session: give it the address first; the browser then asks which scene to build in.
# URL: prints a loopback URL; the launcher is prefilled with the address
python -m site_kg.cli live https://docs.example.com
# skip the launcher with a scene: crawl -> stream -> settle in one view
python -m site_kg.cli live https://docs.example.com --scene neural
# local docs/code directory
python -m site_kg.cli live --local ./docs --scene planets
# replay an ingested site from its stored graph
python -m site_kg.cli live my-site --replayThe page streams the build over SSE while the server crawls and lays the graph out incrementally: nodes arrive in corpus order, edges bind as pages are parsed, and derived relation dimensions stay hidden by default and never participate in layout springs, so they cannot distort the picture. Scenes: Planets (1) chapters as worlds in orbit, Neural (2) a dense field of synapses, City (3) one page per tower — floors follow the page's rank, and the lit windows are the links it emits. #scene=, the last-picked scene in localStorage, and the MCP scene= argument are presets once a scene has been chosen — they never replace the address-first start.
Limits and fallbacks:
The scale floor is the measured 75,415-node / 61,615-edge corpus; layout runs on the server (no browser-side force layout at that size).
--max-nodestrims the preview only.Labels are budgeted at most 40 chapter / 12 tick labels, and the scene is merged point/line batches — no per-node objects or DOM labels.
The live scene draws on WebGPU first (hand-written WGSL; the same layout, birth, drag and finale state feeds both backends, so the two cannot drift). A browser with no
navigator.gpuor no usable adapter falls back to WebGL2 on the same canvas, and without WebGL2 to a canvas-2D view capped at 5,000 nodes that points at the offline archive for the full graph. WebGL context loss is restored in place.The archive viewer (
site-kg render <site_id>) is a single offline HTML file (binary payloads embedded by default;--external-datawritessk-*.binbeside it) and replays the same scenes.
Security: the server binds 127.0.0.1 only, sends no CORS headers, rejects non-loopback Host values, refuses path traversal, serves no corpus files, and offers an optional shared token (--token). With a token, the printed URL carries ?token=… and the page forwards it on snapshot, SSE and start calls. The Three.js r186 bundle is committed and pinned; normal development and offline use need no npm — see tools/build_viewer_bundle.mjs for the deliberate upgrade path.
Relation dimensions
Link and import edges are honest but sparse: most pages in most corpora are
unconnected because nobody linked them. --relations (CLI), relations=
(MCP) or SITE_KG_RELATIONS (env) adds derived dimensions, chosen per
node from that node's own content — not one fixed extractor over everything:
kind | needs | meaning |
| code | this file mentions an identifier defined in another file |
| prose ≥ 220 chars | sparse tf-idf cosine over prose + title tokens: same topic, no explicit link |
| 1–12 dir mates | same source directory / URL folder cohort |
| version in id/URL | 2.0 supersedes 1.4 of the same page family |
| ≥ 3 headings | document-skeleton overlap (Jaccard ≥ 0.35 over heading sets): same-generator API pages |
| ≥ 2 curated terms | shared writer-marked vocabulary: inline code spans, headings, declared symbols |
| frontmatter sha256 | identical content (exact-hash tier, chained) |
| evidence links | bibliographic coupling: both pages cite the same targets |
| evidence links | co-citation: the same sources cite both pages (relates stubs) |
| chat model | pages share ≥ 2 LLM-extracted named entities |
| embed model | embedding cosine over title + body head |
A stub gets no lexical neighbours, a prose page gets no symbol edges, a release
note gets version lineage, a source file gets symbol references. Modes: auto
(keyless, default), llm (auto + an LLM router that may only narrow each
node's set, plus the two model-backed dimensions), all, off, or an explicit
comma list.
Two invariants keep this from becoming a hairball generator: a pair the corpus
already links is never re-labelled with a derived kind, and everything is
per-node gated and fan-out capped. Content-derived dimensions run even on a
corpus with zero cross-references — content is the only signal such a corpus
has — but the verdict still reports NOT-READY (no link structure), and a stub
corpus still gates to zero edges. No LLM or embedding model is required; without
one those dimensions are skipped with a note.
Design and the literature it rests on (AutoSchemaKG arXiv:2505.23628, KGGen arXiv:2502.09956, EDC arXiv:2404.03868, Adaptive-RAG arXiv:2403.14403): docs/RELATIONS.md.
Measured on a 2,000-file DriveOS SDK slice: 132 evidence edges → +142
mentions_symbol, +3,560 similar_to, +427 sibling_of, 4.1 s end-to-end.
Verified
Measured on the embedded vendor 7.0.3 docs site (static Sphinx): 40-page bounded ingest → 40 docs, 32 ref edges, 1709 indexed terms, verdict READY; all seven MCP tools exercised over real streamable-HTTP sessions (2026-10-06). The same origin site proves raw websites — not just wiki exports — carry enough link structure to graph.
ask verified against an on-prem vLLM gateway (qwen3.5-122b chat + bge-m3 embed + bge-reranker-v2-m3): an English question returns a cited step-by-step answer; a Chinese cross-lingual question still answers when keyword recall fails on the ASCII index, because embedding similarity over title + body head is unioned with keyword hits on every query.
Second-site check on the Doxygen API reference (drive-os-linux-sdk-api-ref, 300-page bounded ingest): the first attempt correctly reported NOT-READY (JS shell => BFS discovered 1 page); after the navtree manifest source landed, 301 docs / 527 ref edges, verdict READY, and ask returned a correct cited answer across header/source pages.
Site-type coverage (measured)
engine | manifest tier used | result |
Sphinx (embedded vendor guide) |
| READY, 40 docs / 32 edges |
Doxygen (vendor platform API reference) |
| READY, 301 docs / 527 edges |
MkDocs Material (squidfunk) |
| READY, 60 docs / 2801 edges |
Docusaurus (Checkly) |
| READY, 60 docs / 460 edges |
VitePress (Vue guide) |
| READY, 50 docs / 2402 edges |
MediaWiki (Arch Wiki) | same-site BFS | READY, 44 docs / 1201 edges |
robots-disallowed (docs.astral.sh, docusaurus.io) | n/a | reported |
Every pretty-URL shape is handled (trailing slash, extension-less, /title/X); a leaf seed that discovers <=2 pages returns a seed_hint telling you to seed the docs root.
ask uses block-level embedding selection, not head truncation: the same question on the same corpus went from "insufficient information" to a fully cited answer after that fix (the answer paragraph sat mid-page in a long FAQ).
Client-rendered SPAs: pages that fetch as empty framework shells are detected (empty #app/#root root, or near-zero visible text and no internal links) and re-rendered with headless Chromium (Playwright). Static fetch always runs first — only shell pages pay for a browser. Verified with a local CSR fixture: BFS discovers the JS-injected links, the graph reaches READY, and the JS-rendered body lands in the corpus.
FAQ
The ingest says
blocked by robots.txt. The site disallows this crawler's user agent. Ask the owner, or passrespect_robots=falseonly if you are authorized to crawl it. For deep crawls of sites you own or are authorized to test, use--dev-crawl(CLI) /dev=true(MCP): robots.txt and politeness delays are skipped and the defaults deepen (--max-pages 2000 --max-depth 8, concurrency 8).It found only 1-2 pages. Your seed URL is probably a section leaf; re-ingest with the docs root (the
seed_hintfield says so). If the site is a client-rendered SPA, the Playwright fallback handles shell pages automatically.Do I need an LLM? No for crawl/search/graph/neighbors. Only
askneedsSITE_KG_LLM_BASE+SITE_KG_LLM_KEY(any OpenAI-compatible gateway; vLLM/new-api verified).NOT-READY verdict. The site has no internal cross-references — it is a directory tree, not a graph. You still get full-text search, page bodies (with extracted headings and description in the frontmatter), and the keyless content dimensions (
similar_to,shares_heading, …) still connect pages that share substance; what you do not get is a link-structure claim.
Deployment
Local-first. A sudo-free systemd user unit:
# ~/.config/systemd/user/site-kg.service
[Service]
WorkingDirectory=%h/develop/site-kg
ExecStart=%h/develop/site-kg/.venv/bin/python -m site_kg.cli serve --transport http --port 8766
Restart=on-failure
[Install]
WantedBy=default.targetRoadmap
Semantic enrichment (optional): cognee
cognifyover the crawled corpus whenSITE_KG_LLM_KEYis present — entity/relation edges alongside the structural ones.Cross-file symbol edgesdone:mentions_symbol(regex layer, keyless). tree-sitter symbol/reference edges (aider-style tags) remain an optional extra for precision.Crawl4AI deep-crawl for JS sites that defeat the Playwright shell-fallback (infinite scroll, heavy anti-bot), via the
.[crawl]extra.Static graph visualizationdone:render_siteemits a self-contained 3D Three.js viewer per site (bundle embedded, works offline; seeded layout computed at render time).render_buildadds a WebGPU replay of the construction itself — nodes and per-dimension edge waves animating onto the shells with a live HUD.
License
MIT. Crawled content remains the property of its respective owners.
Available Tools
10 toolsaskB
LLM Q&A grounded in the site's pages: keyword retrieval + graph-neighborhood expansion, then embedding rerank and a cited answer. Requires SITE_KG_LLM_BASE and SITE_KG_LLM_KEY in the server environment.
Inputs: site_id (required), question (required), top_k.
get_pageB
Full markdown body of one page, with title and source URL.
Inputs: page_id (required), site_id (required).
ingest_localA
Build a knowledge graph from a local directory of docs and/or code files.
Docs (md/markdown/mdx/txt/wiki/rst/adoc) are indexed as-is with edges from markdown links and [[wikilinks]]; code files contribute import/include edges and searchable symbol headers. Returns site_id plus a graph-quality verdict: READY (traversable edges found) or NOT-READY (no links/imports; use search).
relations selects the derived relation dimensions, chosen per node:
"auto" (keyless: mentions_symbol/similar_to/sibling_of/supersedes/
shares_heading/shares_term/duplicate_of/coupled_with/co_cited_with),
"llm" (auto + an LLM router plus entity/embedding similarity), "all", "off",
or an explicit comma list. Omit it to use $SITE_KG_RELATIONS, or "auto"
when that is unset. Content-derived edges also run on corpora with no
cross-references; the verdict still reports NOT-READY for those.
Inputs: path (required), max_files, relations, respect_gitignore.
ingest_urlA
Crawl a website, build its knowledge graph, and register it as a queryable site.
Returns site_id for later tools plus a graph-quality verdict: READY (traversable edges found) or NOT-READY (tree without links; graph traversal useless).
relations selects the derived relation dimensions, chosen per node: "auto"
(keyless dimensions), "llm" (LLM router + entity/embedding similarity),
"all", "off", or an explicit comma list. Omit it to use $SITE_KG_RELATIONS,
or "auto" when that is unset.
dev is the development/testing mode for sites you own or are authorized
to test: it ignores robots.txt and politeness delays so a deep crawl of an
authorized target finishes fast. It forces respect_robots=False.
Inputs: url (required), dev, max_depth, max_pages, +3 more.
list_sitesA
List ingested sites with their verdicts and graph sizes.
Inputs: no named parameters. Returns: result
neighborsB
Subgraph around a page up to depth hops (capped at 2).
Use after search to expand context.
Inputs: page_id (required), site_id (required), depth.
render_buildB
Render the graph construction process as a self-contained WebGPU viewer (human inspection path). Replays the build: nodes are born in corpus order onto chapter rings, then evidence edges (ref/imports/contains), then one flying wave per derived relation dimension -- every edge grows from its source node and binds to its target with a pulse, while a HUD counts nodes, edges and per-dimension edges as they land. Returns the file path; AI-facing queries stay on search/neighbors/ask. Needs a WebGPU-capable browser.
Inputs: site_id (required).
render_siteB
Render the graph as a self-contained HTML viewer (human inspection path). Archive mode returns the file path; mode="live" replays the stored graph over a loopback SSE server and returns {live_url, run_id, last_seq}. AI-facing queries stay on search/ask/neighbors.
Inputs: site_id (required), mode, scene, max_nodes.
searchC
Full-text search over the site's pages (inverted index, idf-weighted).
Inputs: query (required), site_id (required), limit.
site_statsB
Graph health for one site: doc/edge counts, edge types, verdict, top hubs.
Inputs: site_id (required).
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 10 tools
The tools mostly target distinct operations: ingestion, listing/stats, search, page retrieval, neighbor expansion, rendering, and Q&A. The only mild overlap is render_site vs render_build, since both produce visual viewers, but their descriptions distinguish archive/live graph viewing from build-process replay.
All names use lowercase snake_case, which keeps them readable. However, the set mixes verb_noun names (list_sites, ingest_local, get_page, render_site) with bare nouns (site_stats, neighbors) and bare verbs (search, ask), so there is no fully predictable naming pattern.
Ten tools is well-scoped for a knowledge-graph ingestion and query server. Each tool covers a distinct phase: ingest, list/inspect, query, expand, render, or ask.
The surface covers the core lifecycle: build graphs from local/URL sources, list sites, inspect graph health, search/get pages, expand neighbors, render, and ask grounded questions. Minor gaps remain around site deletion, re-ingestion/update management, or explicit graph export, but normal query workflows are covered.
Maintenance
Related MCP Connectors
Turn any public website into an MCP server for agents to search, read and navigate.
Scrape, crawl and search the web for AI agents via MCP.
Pages, crawls and URL batches as clean Markdown and JSON for AI agents, with an Evidence Record.
Web search, URL content extraction to Markdown, site mapping, and recursive web crawler.
Related MCP Servers
- AlicenseNot gradedqualityBmaintenanceEnables AI agents to read public web content as clean Markdown, search a private library of previously read documents, and access these tools via MCP, REST API, CLI, and WebMCP.4 npm22MIT
- AlicenseNot gradedqualityAmaintenanceEnables MCP clients and AI agents to fetch individual webpages or crawl sites and receive clean, link-preserving Markdown with caching and robots.txt-aware rate limiting, reducing token usage for downstream pipelines.MIT
- AlicenseNot gradedqualityAmaintenanceEnables AI agents and MCP clients to fetch web pages or crawl sites and convert them into clean, token-efficient Markdown while preserving links.MIT
- AlicenseNot gradedqualityAmaintenanceEnables AI agents and MCP clients to fetch web pages and crawl sites locally, converting content into clean Markdown with preserved links, caching, robots.txt compliance, and minimal token usage.MIT