MDDB reads C, C++ and assembly
The first questions anyone asks of a C codebase are rarely about prose.
Which file defines parse_request? What calls buf_alloc? Which files
include <net/conn.h>?
MDDB 2.18 answers them. Store a C project one document per file, and MDDB records what each file defines, calls and includes. An agent working over MCP can then follow those links instead of guessing from a text search. This post covers what changed, how it was checked, and the limits worth knowing before you rely on it.
What a .c file was before
MDDB has known about source code since 2.12, but only for the languages of a
web theme: CSS, JavaScript and HTML. .c, .h and .S were not on the list,
so a C file was indexed as prose unless the client sent kind: ["code"]
itself.
For C, prose indexing is the wrong choice. The prose tokenizer stems words,
drops stop words, and splits on every character that is not a letter or a
digit. buf_alloc went into the index as two words, buf and alloc, and
the identifier itself could not be found. Even with kind: code set by hand,
a C file got code tokenizing but no symbols, so
code_graph had no edges for it.
What MDDB reads from C now
Take src/server/request.c:
#include <net/conn.h>
#include <util/buf.h>
#include "request_internal.h"
static int parse_header(struct conn *c, struct buf *b);
int
parse_request(struct conn *c)
{
struct buf *b = buf_alloc(c->rx_len);
if (parse_header(c, b) != 0)
return (conn_close(c, ERR_BAD_REQUEST));
return (dispatch(c, b));
}
Stored with no metadata at all (the extension is enough), the document gets:
language: c
defines: parse_header, parse_request
uses: buf_alloc, conn_close, dispatch
imports: src/net/conn.h, src/util/buf.h, src/server/request_internal.h
The rules behind those three lists:
- defines: function definitions,
staticprototypes (the definition is in this file),struct,unionandenumtags,typedefnames,#definemacros and file-scope variables. A prototype withoutstaticdeclares something defined elsewhere, so it does not count. Otherwise every header would claim every function it declares. - uses: names called inside function bodies that the file does not
define. Keywords are skipped, and so are calls through a struct member
(
c->ops->send(c)), because those are not global symbols. - imports:
#includetargets.
Assembly gets a smaller version of the same: ENTRY(memcpy_fast) and .globl
become defines, call, bl and jal targets become uses. So when a C file
calls a routine written in assembly, the edge leads into the .S file.
Include paths
#include "request_internal.h" is relative to the including file, and MDDB
resolves it that way. #include <net/conn.h> is different: the compiler finds
it through -I. A collection now has the same setting:
curl -X PUT "$MDDB/v1/collection-config" -H 'Content-Type: application/json' \
-d '{"collection":"myproject","code":{"includePaths":["src"]}}'
With ["src"], <net/conn.h> in any file is stored as src/net/conn.h,
which matches that header's document key. Set it before loading the code.
Documents written earlier keep their old imports until they are written
again.
Asking the questions
Once the code is in, the questions from the top of this post are graph queries:
# What breaks if I change this header? Callers and includers.
curl -s "$MDDB/v1/code-graph?collection=myproject&key=src/net/conn.h&direction=in"
# What does request.c depend on?
curl -s "$MDDB/v1/code-graph?collection=myproject&key=src/server/request.c&direction=out"
Each edge has a kind and the symbol that justifies it. C files produce
calls edges (a use matched to a define in another file) and imports edges
(an include matched to a document key). The same queries are available to an
agent as the MCP tool code_graph. "Which file defines parse_request" is a
plain metadata filter: meta.defines=parse_request.
How it was checked
A scanner is only worth something if it agrees with the code, so before shipping we ran the extractor over a large open-source C codebase:
- 15,617
.c,.hand.Sfiles in about 5 seconds, single threaded, with no failures on malformed or unusual input. - For a widely included header, the extractor and
grepagreed on the number of files that include it: 730 each. - For one well-known function, the extractor found no callers. That looked
like a bug, but
grepagreed: the name appeared only in its definition, its prototype and comments. The extractor ignores comments, and a prototype is not a call.
That last point is the kind of answer a text search gets wrong. A search for the name returned five files, and none of them was a caller.
The first run also caught a real mistake. Macros invoked at file scope,
such as static REGISTER_HANDLER(...) or DEFINE_COMMAND(...) { }, came out
as function definitions. A name written in capitals only is now treated as a
macro. A variable declared with a macro type, such as
static LIST_HEAD(, conn) idle_conns;, is recorded under its own name.
Limits
This is a scanner, not a compiler, and some answers it cannot give:
- No preprocessing. Both branches of an
#ifdefare read, and nothing is expanded. A function whose name only appears as a macro argument is not recorded as a definition. For example,DEFINE_COMMAND(status, cmd_status) { ... }definescmd_status, and the extractor does not record it. Its calls are still recorded. - Indirect calls are not followed. Calls through a function pointer, or through a table of them (an operations vector, a list of callbacks), are not edges. In code that dispatches that way, a lot of the call structure is invisible to it.
- The capitals rule is a convention, not a fact. In a codebase that writes functions in capitals, those functions would be missed.
- The lists are capped at
MDDB_CODE_MAX_SYMBOLS(512 by default). A generated register header with 90,000#defines keeps its first 512.
None of this replaces cscope, ctags or clangd on your own machine. The point is different: a model answering questions over MCP gets a first answer grounded in the code, with the file and symbol to read next.
Every other language
C is the newest language, not the only one. Here is what MDDB does with each file type, based on the extension, when the client does not say otherwise:
| Files | Code search and chunking | Symbols and code_graph |
|---|---|---|
| CSS, SCSS, Sass, LESS | yes | selectors, custom properties, @import, url() |
| JavaScript, TypeScript | yes | functions, classes, import / require |
| HTML | yes | ids, classes used, src / href, on* handlers |
| C, C++ (new) | yes | as above |
Assembly .S .s .asm (new) |
yes | ENTRY / .globl, calls, includes |
| m4 (new) | yes | not yet |
| Go, Python, Ruby, PHP, Rust, Java, shell, SQL | yes | not yet |
| JSON, YAML, TOML, XML, SVG | yes | not yet |
"Code search and chunking" means:
- Search: a whole identifier and its parts are both searchable
(
parse_request, and alsorequest). Keywords such asifandforstay in the index, and nothing is stemmed. - Embeddings: content is split where brackets balance, so a chunk sent for embedding is a whole function rather than half of one.
That has worked since 2.12 for every language in the table not marked new. Symbols are what each language has to earn with its own extractor. If you need one for a language we have not covered, open an issue with a real file and the questions you want answered.
Upgrading
2.18 is backward compatible. To get symbols for C documents you already store, write them again: symbols are extracted when a document is saved. The full list of changes is in the changelog, and the extraction rules are in the API reference.