duckdb-code
A frugal, embeddable code-intelligence building block for agentic systems — MCP tools that help an IDE or CLI explore and change a codebase over a local DuckDB file, no external service. Impact analysis and structure explanation over the dependency graph is the first capability.
duckdb-code is a python package that exposes the actual structure of a
codebase to a local agent, in 3 steps:
- analyze a folder of source files with tree-sitter and extract nodes and edges
- store that graph in a single DuckDB database baked next to your agent
- answer
impact,explain, andreportquestions with a local function call
When your agent needs structural code questions
Most agents reason over code they cannot see the shape of: they read files one at a time and guess at the blast radius of a change. The honest requirement is small and structural — given a symbol or a file, what depends on it, what does it depend on, and what is the whole thing shaped like. The reflex answer is large: stand up a code-intelligence service, index into a hosted graph database, wire the agent to it over the network. For one repository, read-mostly, that is a lot of moving parts to own.
For the common case — one repository, packaged with one agent — you can skip
the external service entirely. Index the code into a single DuckDB file, keep
that file next to the agent, and answer impact / explain / report with a
local function call, no network round-trip. Where
duckdb-vectors answers "what
text is semantically near this query", duckdb-code answers "what code is
affected if this symbol or file changes" and "what is the structure of this
code". The must-be-reliable deliverable is impact: a transitive
reverse-dependency set over the edge graph, with every best-effort edge flagged
rather than silently dropped.
Data architecture decisions and facts
A few deliberate choices shape how duckdb-code indexes and serves:
- One DuckDB file is one graph — no external graph service and no network hop on the query path.
- Code and Markdown stay in separate domains in the same database: a Markdown link never binds to a code symbol and a code edge never binds to a page, so the two corpora share one file without crosstalk.
- Incremental, etag-driven re-index:
indexis incremental by default. A full rebuild runs on an empty database or withrebuild=True; otherwise only changed, added, and deleted files are reconciled by comparing each file's storedetagagainst a freshly computed one. The incremental result is identical to a full rebuild of the same on-disk state. - Read-write everywhere: the store always opens read-write, since
incremental updates are the rule — the
mcpserver can index its target folder on startup (--index) and refresh it in place, rather than serving a frozen file.
Keeping the graph fresh in an agentic IDE
Because re-indexing is incremental and cheap, the graph can track the working tree as an agent edits it, rather than going stale the moment code changes. Three mechanisms keep it current:
duckdb-code indexreconciles only what changed since the last run (etag-driven), so a re-index after an edit is a near-instant no-op when nothing moved.duckdb-code watch <folder>runs as a long-lived process and re-indexes incrementally on every relevant file change, so the graph stays live with no manual step — the natural fit for an IDE session where the agent is actively editing.duckdb-code mcp --index <folder>folds a startup incremental build into the MCP server, so a freshly launched agent serves a graph that already reflects the current source.
The upshot: an agent asking impact mid-session sees the blast radius of the
code as it is now, not as it was at the last manual index.
Install and run
With uv, there is nothing to install first.
Run the tool with uvx and uv fetches the package and its dependencies on the
first call and reuses the cache after:
$ uvx duckdb-code --help # run without installing
$ uvx duckdb-code index ./my-project # index a folder
For a persistent duckdb-code on your PATH (handy when an MCP client config
wants an absolute command path), install it as a uv tool:
$ uv tool install duckdb-code
$ duckdb-code --help
Prefer pip? pip install duckdb-code into a virtual environment works the
same way.
To run an unreleased build — a git ref or a local checkout — use the same
commands with --from:
$ uvx --from git+https://github.com/bernard357/duckdb-code duckdb-code --help
$ uvx --from /path/to/duckdb-code duckdb-code --help # from a local clone
SDK entry point
Open a store against a local .duckdb file and query it. The store always
opens read-write, since incremental updates are the rule for this package:
from duckdb_code import CodeStore
store = CodeStore(db_path="code.duckdb")
result = store.impact("payments.charge.Charge")
for hit in result.affected:
print(hit.distance, hit.resolution, hit.node.qualified_name)
explanation = store.explain("payments.charge.Charge")
report = store.report()
impact returns the affected set with per-node distance and resolution;
explain returns a node's defines/fan-in/fan-out/bases/subclasses; report
introspects the index.
store.index(folder) is incremental-by-default: a full rebuild runs when the
database is empty or you pass rebuild=True, and otherwise only changed, added,
and deleted files are reconciled by comparing each file's stored etag against
a freshly computed one (the compute_etag content fingerprint). The global
cross-file resolution is re-run so edges that bound to a changed file are
re-resolved; a no-change re-index is a no-op. The incremental result is
identical to a full rebuild of the same on-disk state.
from duckdb_code import CodeStore
store = CodeStore(db_path="code.duckdb")
store.index("./my-project") # full build on an empty database
store.index("./my-project") # incremental re-index (etag-driven)
store.index("./my-project", rebuild=True) # force a full rebuild
Indexing from the CLI
$ duckdb-code --version # print the version and exit
$ duckdb-code index ./my-project --db code.duckdb # incremental by default
$ duckdb-code index ./my-project --db code.duckdb --rebuild # full rebuild
The index verb is incremental-by-default: it runs a full build when the
database is empty and reconciles only changed, added, and deleted files on a
re-run. Pass --rebuild to force a full rebuild.
By default the indexer skips the noise that should never be part of a code graph, so it never descends into dependency or build trees:
- the
.gitdirectory and the common dependency/build/cache directoriesnode_modules,.venv,venv,vendor,dist,build,__pycache__,.mypy_cache,.pytest_cache, and.ruff_cache; - files larger than 1 MB (1,000,000 bytes — a file of exactly that size is kept);
- anything matched by a
.gitignoreor an optional.duckdbgraphignore, honored hierarchically like git: every directory's ignore files apply to that directory and its descendants (patterns relative to the file's own directory), so nested ignore files — e.g. a per-repo.gitignoreunder an indexed parent — are respected, not only one at the indexed root.
Pass --no-default-ignores (CLI) or use_default_ignores=False (SDK) to
disable the built-in directory and size exclusions when you do want a vendored
tree in the graph; the .gitignore / .duckdbgraphignore rules are still
honored. Excluded files that the walk reaches (oversized or ignore-matched) are
counted in the report's combined skipped total; whole pruned directories are
never walked and so cost nothing.
Indexing a folder of Markdown pages
The same graph indexes a folder of Markdown pages, so a knowledge base of
interlinked notes becomes a graph alongside code. Point index at a Markdown
folder:
$ duckdb-code index ./my-notes --db code.duckdb
Every .md / .markdown file becomes a page node with heading and tag
nodes, and the links between pages become edges: a relative [text](other.md)
link or a [[Wikilink]] becomes a links_to edge, and an image embed becomes
an embeds edge. The same impact, explain, and report queries then answer
knowledge-graph questions:
- what links here —
impacton a page returns the pages that link to it. - a page's structure and its links —
explainon a page returns its inbound and outbound links together with its heading structure. - orphan pages — a page with zero inbound links (empty
impact) is unreferenced by the rest of the corpus. - corpus counts —
reportsurfaces the page, heading, and tag counts and the link-edge totals.
Code and Markdown stay in separate domains: a Markdown link never binds to a code symbol and a code edge never binds to a page, so the two corpora can share one database without crosstalk.
Semantic / vector search is out of scope for duckdb-code — it answers
structural, link-based questions. For "what text is semantically near this
query", use the sibling
duckdb-vectors package.
MCP tool
duckdb-code mcp is an MCP-over-stdio server exposing twelve query tools:
search(query, limit=…)— ranked semantic search for symbols by intent, when you don't know the name.find_similar(target, limit=…)— code built like a given symbol or snippet (house patterns, duplication).overview(budget=…)— repo map: modules, entry points, and most central symbols, sized to a token budget.related_tests(target)— the tests to run after editing a symbol or file.history(target)— git signals: recent commits, co-change partners, churn.links(target)— cross-boundary edges an LSP never sees together: HTTP route↔client, Markdown links/embeds/tags.impact(target, …)— transitive reverse-dependency set for a symbol or file.report()— introspects the index: database path, schema version, languages, and node/edge counts.export(out=…, inline=…)— returns the whole graph as{meta, nodes, edges}JSON, writing it to a file and returning a summary by default (inline only for a small graph).analytics(kind)— whole-repo views:cycles,clusters,dead_codecandidates,hotspots(centrality × churn).index_status()— index freshness: the indexed commit, a working-tree dirty flag, when it was built, and the node/edge counts.context_for_change(base=…)— the symbols a diff touches plus their one-hop neighbourhood, for review and PR work.
explain (a symbol's structural neighbourhood) is on the SDK and the
duckdb-code explain CLI verb, but is not advertised over MCP — overview and
context_for_change cover it there.
These twelve are the first slice of a larger, LSP-complementary tool contract:
whole-repo, precomputed questions (intent search, one-call impact,
cross-boundary links, history) that a Language Server structurally can't
answer, while point queries (definition, references, hover, rename,
diagnostics) stay with the LSP — every result carries path + range for the
hand-off. The full target surface, each tool's JSON shape, and the build order
are in the MCP tool contract.
An MCP client spawns duckdb-code mcp as a child process in the same
container and talks to it over stdin/stdout; stdio is a same-container
transport, not a network protocol.
The server also serves over Streamable HTTP, selected with
--transport http:
$ duckdb-code mcp # stdio (default)
$ duckdb-code mcp --transport http # http on 127.0.0.1:8000/mcp
$ duckdb-code mcp --transport http --host 127.0.0.1 --port 8000
HTTP binds loopback 127.0.0.1:8000 at path /mcp by default; pass --host
and --port to override. Both transports expose the identical impact,
explain, and report tools with the same JSON-object shapes. The HTTP
transport is the groundwork that will later let a client index a repository in
a separate sandbox (for example a GitHub repository); that remote / sandbox
indexing is future work this transport enables, not something shipped here.
Pass --index <folder> (or set DUCKDB_CODE_ROOT) to index that folder on
startup before serving — an incremental build, so the served graph reflects the
current source without a separate index run. Build progress goes to stderr
(stdout carries the MCP stdio protocol) and a failed build aborts startup. Pass
--no-startup-index, or a bare --db with no folder configured, to serve a
prebuilt database as a frozen snapshot. This is a one-shot startup build, not a
watcher.
Setup for working on the package itself
Requirements:
- Python 3.12+
uv(Python env and builds)
Shell commands:
$ make setup # install the Python toolchain into a local venv
$ make test # run the test suite
$ make lint # ruff, mypy, markdownlint
The Makefile is the toolbox
Drive common tasks through make; do not call the underlying tools directly:
make- list common targetsmake setup— check and install the development toolchainmake lint/make format— check / apply ruff, mypy, markdownlint.make test— run the pytest suite.make self-test— capability evidence: index the package's own source withduckdb-codeand run a sequence of labelled requests (list files, list functions, change-impact traversal, cross-module symbol context, index overview, most-depended-upon hotspots), each printing a result and asserting a structural fact. Dogfoods the tool on a real multi-module codebase, distinct from the hermeticmake testsuite.make build— build the wheel and sdist (version from the git tag via hatch-vcs).
Project structure
duckdb-code/
├── src/duckdb_code/ # the package
│ ├── store.py # the CodeStore API
│ ├── indexer.py # folder → DuckDB graph index
│ ├── cli.py # the duckdb-code CLI (incl. the mcp verb)
│ ├── mcp_server.py # the MCP-over-stdio server
│ ├── config.py # Settings / DUCKDB_CODE_* env
│ ├── models.py # Node / Edge / ImpactResult pydantic models
│ ├── etag.py # compute_etag content fingerprint
│ ├── extractors/ # pluggable extractors (python, javascript/typescript, markdown)
│ └── embedders/ # pluggable embedders (fastembed default, hashing fallback)
├── tests/ # pytest suite
├── pyproject.toml # metadata, deps, tag-driven version (hatch-vcs)
└── Makefile # the task toolbox
.md / .markdown files index into the same graph as code. Releases are
tag-driven: the version comes from the git vYYMMDD tag, and pushing a tag
publishes to PyPI.
Configuration
Settings resolve with the precedence: explicit argument >
DUCKDB_CODE_* environment variable > default.
| Setting | Argument | Environment variable | Default |
|---|---|---|---|
| Database path | db_path |
DUCKDB_CODE_DB_PATH |
code.duckdb |
| Indexed source root | root |
DUCKDB_CODE_ROOT |
None |
Pointers
- Worked examples: documentation/examples.md — the
make self-testrequests, explained with observed results. - Documentation site: https://bernard357.github.io/duckdb-code/
- PyPI project: https://pypi.org/project/duckdb-code/
License
MIT.
Metadata
Release files for duckdb-code 261101
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| duckdb_code-261101.tar.gz | 209.2 kB | With provenance | Details for duckdb_code-261101.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| duckdb_code-261101-py3-none-any.whl | Python 3 | none | any | Details for duckdb_code-261101-py3-none-any.whl |
Total release size: 300.7 kB
Release files / duckdb_code-261101.tar.gz
| Download URL | duckdb_code-261101.tar.gz |
|---|---|
| Size | 209.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
890e5806b36231c9e90685a0c82eef28de63efe2a7e989f7a63050890fb65afd
|
|
BLAKE2b-256 checksum How to use checksums |
52dc80931e52869556b8506cab5241799098e790d4c8449f3a2e143dd29341f2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 1, 2026.
Transparency logRelease files / duckdb_code-261101-py3-none-any.whl
| Download URL | duckdb_code-261101-py3-none-any.whl |
|---|---|
| Size | 91.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0c3006781285b8da70dd83fca971beaa7acda8054b558ab8956307a967aa3cfd
|
|
BLAKE2b-256 checksum How to use checksums |
bd966bf708cfb853753e7c12cb0d266c345af2bc1a2511e41e381112b5ac8036
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 1, 2026.
Transparency logSecurity for duckdb-code 261101
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for duckdb_code-261101.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 1, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.