mcp-gtags-server
It is an MCP server that gives AI coding agents fast, indexed code navigation (definitions, references, call graphs, and more) powered by GNU Global, replacing slow tree-wide greps across kernel-scale C/C++ and mixed-language repos.
find_definition — locate every definition of a symbol with its #ifdef guard stack, ctags kind/signature, usage summary, and EXPORT_SYMBOL status; resolves macro-generated names like sys_read → SYSCALL_DEFINE3(read, ...)
find_references — list all usage sites (with guards), auto-grouped per-file for hot symbols, narrowable by path_prefix; fallback covers libc calls and variables
get_symbol_body — read exactly a function/struct/macro body, not the whole file
find_callers — deduplicated list of who calls a function, with call counts and each first call site
find_callees — what a function calls, split into in-tree locations and external names
reachability — shortest static call chain from function A to B, or an honest no-static-path answer
list_file_symbols — a file's API surface: every function/struct/macro it defines
update_index — synchronous index refresh (incremental or full rebuild) after edits
Kernel-aware filtering — pass a kernel .config to drop definitions/references whose guards are definitely false; case-insensitive matching, pagination, and compact text or JSON output
Multi-language — native C/C++/Java/PHP/Yacc/asm plus ~150 languages (Python, Go, Rust, JS/TS, Ruby) via ctags/Pygments
Zero maintenance — index auto-builds on first query, auto-refreshes in the background, and the toolchain self-installs; also ships MCP prompts like /gtags:impact and /gtags:explain
Provides indexed code search and navigation capabilities using GNU Global's tags engine, enabling fast definition lookup, reference search, call hierarchy, and code exploration for large codebases.
mcp-gtags-server
Stop letting your AI agent grep. Give it an index.
mcp-name: io.github.harshithsunku/mcp-gtags-server
# If you don't have uv yet (one-time, no sudo):
curl -LsSf https://astral.sh/uv/install.sh | sh{ "mcpServers": { "gtags": { "command": "uvx", "args": ["mcp-gtags-server"] } } }One user-level config entry, no sudo, no pre-installed anything — the whole toolchain installs itself into user space on first use, and every repo you open is served automatically.
📖 Full documentation — install for every client, the tool reference, output format, kernel features, configuration and troubleshooting.
Every AI coding agent — Claude Code, Cursor, Codex, you name it — answers "where is this function defined?" the same way: grep the entire tree. On a million-line C/C++ codebase that's a full scan per question, and the output is a firehose: every comment, string literal, and unrelated match, dumped straight into the model's context window.
mcp-gtags-server replaces those scans with indexed lookups powered by GNU Global (gtags) — the same tags engine kernel and systems developers have trusted for decades — exposed to agents over the Model Context Protocol. Built for the codebases LSP-based tools can't handle: kernel-scale C/C++, trees that don't currently compile, machines you can't sudo on.
~100× faster per query — milliseconds instead of seconds, at any codebase size
Radically less noise — the definition, not 7,873 lines of matches
Speaks kernel —
#ifdefguard stacks with.configfiltering, macro-generated symbols (sys_read→ itsSYSCALL_DEFINE3site) that no other tagging tool resolves, and definitions the parser misses recovered from theirEXPORT_SYMBOLsite via ctagsZero index management — first query builds the index, every query auto-refreshes it
Correctness measured in CI — a 64-case golden eval covering all 8 tools against a pinned kernel: 100% recall, 100% precision@1 (docs/capability.md)
Works everywhere MCP does — Claude Code, Claude Desktop, Cursor, any MCP client
The numbers (real Linux kernel, not a toy)
Measured on a full Linux kernel checkout — 65,163 C/C++ files, 37.1 million lines — warm page cache:
Question an agent asks |
| gtags (this server) | Context consumed |
Where is | 1.40 s | 0.01 s | 8 lines → 1 line |
Where is | 1.62 s | 0.01 s | 7,873 lines → 5 lines |
Who references | 1.62 s | 0.10 s | 7,873 noisy lines → 2,744 real sites (or a ranked per-file summary) |
Show me | read a 3,500-line file |
| exactly the 271-line function |
Who calls | 245 raw match lines |
| 62 deduped caller functions, with counts |
Where is | no answer — the name is macro-generated | 0.03 s |
|
Where is | 24,774 noisy match lines | 0.2 s |
|
Does | N rounds of grep + reading | 0.6 s | the shortest call chain, with every call site's file:line |
Every | 22,905 tree-wide match lines | 0.01 s |
|
One-time index build: 66 s for the whole kernel. Incremental refresh after edits: well under a second. Reproduce it yourself with scripts/benchmark.sh:
./scripts/benchmark.sh /path/to/linux tcp_v4_rcv kmalloc ext4_readdirThe speed is nice. The real win is precision: an agent that gets 5 exact lines instead of 7,873 noisy ones keeps its context window for actual reasoning.
And the answers are measured, not assumed: CI runs a 64-case golden eval against pinned kernel v6.16 on every push — currently 100% recall, 100% precision@1 across definitions, macro resolution, export recovery, references, callers, callees, definition bodies, #ifdef guards, and reachability, covering all 8 tools. The full methodology, numbers, and honest limitations live in docs/capability.md.
Related MCP server: Sigil MCP Server
Quick start (60 seconds)
Option A — one config entry (recommended)
Add the server once, at user level, and you're done — no installer, no pre-installed gtags, no per-repo setup. The only prerequisite is uv (or use Option B, which installs it for you).
# Claude Code (once per device, all repos):
claude mcp add --scope user gtags -- uvx mcp-gtags-server// Cursor (~/.cursor/mcp.json), or any MCP client's global settings:
{
"mcpServers": {
"gtags": { "command": "uvx", "args": ["mcp-gtags-server"] }
}
}Or click the Cursor one-click install badge at the top of this page.
On the very first tool call the server bootstraps everything else by itself: GNU Global (prebuilt user-space binaries — no compiler, no sudo), universal-ctags, and Pygments, all into ~/.gtags-mcp. While that one-time install runs (a few seconds on most platforms), tool calls return a "toolchain is being installed — retry shortly" status instead of failing. Opt out with --no-auto-setup or GTAGS_MCP_AUTO_SETUP=0.
The prebuilt Linux binaries run on any distro with glibc ≥ 2.28 (RHEL/Rocky 8+, Ubuntu 18.10+, Debian 10+). On older hosts — or if a downloaded binary fails its post-install execution check — setup automatically compiles GNU Global from source instead, which only needs make and a C compiler.
Option B — install the plugin (server + skill + slash commands)
The plugin bundles this server, a "C/C++ code navigation" skill that tells
the agent which tool answers which question, and two slash commands
(/gtags:impact, /gtags:explain). Installing it registers the MCP server for
you — there is no config file to edit.
Client | Install |
Claude Code |
|
Codex |
|
Cursor | Loads Agent Plugins 1.0.0, but installs only from the Cursor Marketplace or a team marketplace — until this plugin is listed there, use the one-click badge or Option A |
Install one way, not both. A plugin and a manual entry run two servers, and the agent sees every tool twice. Switching to the plugin? Remove the manual entry first (
claude mcp remove gtags).
Option C — one-line installer (shared background server)
One command. No sudo. Works everywhere — restricted corporate machines, containers, build servers:
curl -fsSL https://raw.githubusercontent.com/harshithsunku/mcp-gtags-server/main/scripts/install.sh | bashEverything lands in your home directory — the server (via uv), GNU Global, universal-ctags, and Pygments (in ~/.gtags-mcp). When it finishes, a shared background HTTP server is running that every client and IDE window on the machine can point at (http://127.0.0.1:8383/mcp), and the exact client configuration is printed to your console.
Re-run the same command any time:
Up to date? → "Already installed and up to date — nothing to install", and the config is printed again.
New release on GitHub/PyPI? → the package updates, an outdated gtags toolchain is wiped and reinstalled automatically, and the background server restarts on the new version.
Slash commands (MCP prompts)
Available with any install, in clients that support MCP prompts (Claude Code, Cursor). They cost no tool-schema context:
Command | What it runs |
|
|
| definition (+ |
One config, many repos
However you install it, one user-level entry serves every repo you open — 20 repos need zero extra installs and zero extra config. Each tool call resolves its project root down this ladder:
project_rootargument on the tool call (agents pass this to target any tree)--rootflag /GTAGS_MCP_ROOTenv varrootin a config file (note: pinning a root here defeats multi-repo)The client's workspace roots (MCP roots protocol) — IDEs that advertise their open folders get the right repo automatically, even on the shared HTTP server; with several folders open, agents are asked to pass
project_root. Roots exist only for clients on protocol revisions up to 2025-11-25: the 2026-07-28 revision deprecated them, so those clients skip this step (stdio servers still resolve through step 5; on the shared HTTP server, agents passproject_root)Walk up from the server's working directory to the nearest
.git/GTAGS— this is why stdio servers spawned by Claude Code/Cursor inside a repo just work
Step 5 can be switched off with --no-cwd-fallback / GTAGS_MCP_CWD_FALLBACK=0
(the plugin sets it): the tools then ask for project_root instead of indexing
whatever directory the server happens to run in. Plugin clients launch the
server from the plugin's own folder, which is never your project.
That's it. No indexing step, no configuration. Ask your agent "who calls tcp_v4_rcv?" — the first query in any repo builds that repo's index automatically, and every query after that is answered in milliseconds. Run mcp-gtags-server doctor any time to see what the server detects, or mcp-gtags-server config to re-print the client configuration.
The installer runs mcp-gtags-server --transport http --host 127.0.0.1 --port 8383 in the background (pid: ~/.gtags-mcp/server.pid, log: ~/.gtags-mcp/server.log). Environment overrides for the installer:
Variable | Default | Meaning |
|
| HTTP port |
|
| Bind address — set |
| unset |
|
Security note: the HTTP endpoint is unauthenticated. It binds localhost by default; only bind 0.0.0.0 on networks you trust.
# 1. GNU Global — EITHER user-space (no sudo):
mcp-gtags-server setup
# OR a system package:
sudo apt install global # Debian/Ubuntu
sudo dnf install global # Fedora
brew install global # macOS
# 2. The server:
uv tool install mcp-gtags-server # or: pip install mcp-gtags-serverThe server finds binaries in this order: --bin-dir/GTAGS_MCP_BIN_DIR/config bin_dir → ~/.gtags-mcp/bin → PATH → ~/.local/bin.
Easiest: download mcp-gtags-server.mcpb from the latest release, drag it into Claude Desktop's Settings → Extensions, and pick your project folder in the setup screen. The gtags toolchain installs itself on first use.
Or add to claude_desktop_config.json manually (pin the project since Desktop doesn't launch in your repo):
{
"mcpServers": {
"gtags": {
"command": "uvx",
"args": ["mcp-gtags-server", "--root", "/absolute/path/to/your/project"]
}
}
}By default the server uses the client's advertised workspace root (MCP roots protocol) or auto-detects the project root by walking up from its working directory to the nearest .git or existing GTAGS — so queries from anywhere inside a monorepo resolve to the repo root. Override with --root /path, the GTAGS_MCP_ROOT env var, or root in a config file — or pass project_root on any individual tool call to query a different tree. (See "One config, many repos" for the full resolution order.)
Every setting can also live in a TOML file, so teams share defaults through the repo (like .editorconfig):
Project:
.gtags-mcp.tomlat the project rootUser:
~/.config/gtags-mcp/config.toml
# .gtags-mcp.toml
label = "native-pygments" # force a GTAGSLABEL parser label
bin_dir = "/opt/tools/bin" # extra directory searched for gtags/global/ctags
skip_globs = ["*.gen.c"] # never index paths/basenames matching these globs
respect_gitignore = true # default: index only what `git ls-files` reports
enrich = true # default: ctags kind/signature/scope on results
guards = true # default: #ifdef guard stacks on results
macro_resolve = true # default: resolve macro-generated symbols (sys_*, ...)
# root = "/abs/path" # default project root (user config)Precedence: tool-call argument > CLI flag > environment variable > project config > user config > built-in default.
Tools
Eight tools, all read-only except update_index. Each takes project_root and
format ("text" default, "json"); list-shaped tools also take limit/offset.
Full parameter reference: docs → Tools.
find_definition — Go to definition: every definition with its
#ifdefguard stack and ctags kind/signature, plus a usage summary (reference/file counts, hottest files,EXPORT_SYMBOL*status). Resolves macro-generated names (sys_read→SYSCALL_DEFINE3).find_references — Every usage site with its guard stack; above 200 references it returns a per-file distribution, and
path_prefixnarrows it to one directory.get_symbol_body — The source of one definition (function, struct, or multi-line macro), not the whole file.
find_callers — Who calls this function: references mapped to their enclosing functions, deduplicated, with call counts and call lines.
find_callees — What this function calls: call sites taken from its body and checked against the index, split into in-tree (with locations) and external.
reachability — The shortest static call chain from function A to function B, with the file:line of every hop, or "no static path".
list_file_symbols — Every symbol a file defines (its API surface).
update_index — Synchronous index refresh after edits;
full=truerebuilds from scratch. The only tool that writes.
Symbol-level tools — the noise killers
Give the agent the symbol, not the file.
Tool | What the agent gets |
| Just the source of a definition. The 271-line |
| The call graph, deduplicated. Every reference mapped to its enclosing function with call counts: 245 raw lines for |
| The outgoing call graph. What does this function call? Body-extracted call sites, each verified against the index, split into in-tree (with locations) and external. |
| "Can this function end up in that one?" — BFS over the caller graph returns the shortest call chain from A to B with the file:line of every call site ( |
Core lookups
Tool | What it does | Underlying command |
| Where is this defined, and what is it? Definitions with |
|
| Every usage site, each with its guard stack. Above 200 references it returns a per-file distribution instead of a wall of lines ( |
|
| A file's API surface — every symbol it defines |
|
| Synchronous freshness barrier after edits; |
|
Every query tool supports limit/offset pagination, long-line truncation, and (where it makes sense) case_insensitive — output is engineered to never flood a context window.
Every tool also declares MCP tool annotations: all are readOnlyHint: true except update_index, and none reach outside your machine (openWorldHint: false). Clients use these hints to auto-approve and parallelize read-only calls. Each response carries the envelope exactly once, as text content, with no duplicate structuredContent copy.
The flow that saves your context window
1. find_definition("kmalloc") → definitions + #ifdef variants + usage spread (3 lines)
2. find_callers("ext4_mark_inode_dirty") → deduped callers, each with its call line
3. get_symbol_body("tcp_v4_rcv") → read the ONE function that matters
4. find_callees("tcp_v4_rcv") → what it depends on, with locations
5. reachability("ksys_read", "rw_verify_area") → the call chain, one line per hop
6. find_references("mutex_lock", path_prefix="fs/ext4") → hot symbol, one subsystemA few hundred lines of context total — versus tens of thousands for the grep-and-read-files equivalent.
Output and kernel smarts
Output: compact text by default, JSON on request
Since v2.0.0 every tool answers with grep-shaped text — the format agents read best, and 2–3× cheaper in tokens than the same answer as JSON:
kmap: 4 definitions under 3 #if variants · 261 refs in 65 files (top: tools/perf/util/machine.c 26, …)
include/linux/highmem-internal.h:40: static inline void *kmap(struct page *page) [function; #if CONFIG_HIGHMEM]
include/linux/highmem-internal.h:170: static inline void *kmap(struct page *page) [function; #if !CONFIG_HIGHMEM]
next: get_symbol_body, find_callers, find_referencespath:line: source rows carry a tag with what the symbol is and the
#if/#ifdef stack it lives under. Paginated results end with a footer like
[1-100 of 5290 · offset=100 for more].
Against v1.x's JSON default the same seven kernel questions cost 20% fewer
tokens overall — up to 68% on individual lookups. find_callers is the one
call that got bigger (about 9%), on purpose: it now includes each call site's
source line, which is exactly what agents used to spend an extra grep to see.
Pass format="json" for the machine-readable envelope — the same data, rendered
from one source of truth:
{
"tool": "find_definition",
"root": "/abs/project/root",
"results": [
{"symbol": "kmap", "path": "include/linux/highmem-internal.h", "line": 40,
"col": 22, "kind": "function", "typeref": "void *", "scope": null,
"signature": "(struct page * page)", "guard": ["CONFIG_HIGHMEM"],
"snippet": "static inline void *kmap(struct page *page)"}
],
"total": 2, "offset": 0, "truncated": false,
"definition_count": 2, "guard_variants": 2, "reference_count": 261,
"file_count": 65, "top_files": [{"path": "…", "count": 26}], "exported": null,
"next_tools": ["get_symbol_body", "find_callers", "find_references"],
"warning": null
}Symbol locations always use the stable record schema
{symbol, path, line, col, kind, typeref, scope, signature, guard, snippet}with repo-relative paths. Keys are only ever added, never renamed or removed within a major version.kind/typeref/scope/signaturesay what a symbol is (since v0.8.1): function vs. macro vs. struct vs. typedef vs. enum constant, its return/target type, its enclosing scope (enum:color,struct:item), and its parameter list — extracted per file by universal-ctags with no build and no compile database, cached, and filled on definition-shaped results (find_definition,list_file_symbols). When universal-ctags isn't available the fields are simplynull; disable explicitly with--no-enrich,GTAGS_MCP_ENRICH=0, orenrich = falsein.gtags-mcp.toml.guardsays when a symbol exists (since v0.9.0): the enclosing#if/#ifdefstack, outermost first ([]= unconditional,null= scanning disabled or file unreadable). See the next section — this is the headline feature.resolved_viasays how a symbol was found when it took macro-family resolution rather than a literal index match ("macro:SYSCALL_DEFINE","fuzzy:vfs_read") — see Macro-generated symbols.next_toolstells the agent the highest-value follow-up call for what was (or wasn't) found.total/offset/truncateddrive pagination; failures keep the envelope with anerrorfield and are flaggedisErroron the protocol so the agent can self-correct.Composite tools return tool-shaped
results(e.g.find_callees{in_tree, external},reachabilitya hop chain) inside the same envelope.Every tool declares MCP tool annotations: all are
readOnlyHint: trueexceptupdate_index, and none reach outside your machine (openWorldHint: false) — clients use these to auto-approve and parallelize calls.
Upgrading from v1.x? Pass format="json" explicitly wherever you parsed the
old default. symbol_info is now part of find_definition,
summarize_references is find_references (it groups by file automatically),
and blast_radius is the /gtags:impact prompt — git diff, then
find_callers on each changed function.
#ifdef-aware: know which definition your config actually compiles
Kernel and firmware code defines the same symbol multiple times and lets the
build configuration pick one. Every other no-build tool returns a flat,
unexplained list — an agent happily reads the no-op stub of kmap and
reasons its way to a wrong answer. This server reads the preprocessor
conditionals (pure scanning — still no build, no compile_commands.json):
Symbol: kmap
2 definitions under 2 distinct guards:
[CONFIG_HIGHMEM] defined at include/linux/highmem-internal.h:40 — function kmap(struct page * page) -> void *
[!CONFIG_HIGHMEM] defined at include/linux/highmem-internal.h:170 — function kmap(struct page * page) -> void *Pass active_config — a kernel .config path or a macro list like
"CONFIG_SMP,BITS_PER_LONG=64,!CONFIG_DEBUG" — to find_definition,
or find_references, and definitions whose guard stack is
definitely false under it are dropped (the envelope reports the count as
config_filtered). Filtering is deliberately conservative: a .config is a
closed world for CONFIG_* macros (kbuild semantics, including
=m → CONFIG_X_MODULE and IS_ENABLED/IS_BUILTIN/IS_MODULE), but
anything unknown (__ASSEMBLY__, ARCH_HAS_*, arithmetic it can't decide)
never drops a result.
The details are handled so the output stays clean: classic include guards
(#ifndef FOO_H) are detected and suppressed, #elif chains compose into
explicit conditions (!CONFIG_X86_64 && CONFIG_X86_32), comments on
directives are ignored (they lie), and broken/partial files never fail a
query. Disable with --no-guards, GTAGS_MCP_GUARDS=0, or guards = false
in .gtags-mcp.toml.
Macro-generated symbols resolve too (sys_read → SYSCALL_DEFINE3)
The best-known gap of every tagging tool on the kernel: sys_read,
trace_sched_switch, and css_set_lock have no literal definition
anywhere — they are minted by token-pasting macros, and a plain lookup
comes back empty (or worse, returns a same-named test helper). This server
resolves them from the index alone, no preprocessor and no build:
find_definition("sys_read") → fs/read_write.c:723 SYSCALL_DEFINE3(read, ...) resolved_via: "macro:SYSCALL_DEFINE"
find_definition("__x64_sys_openat")→ fs/open.c:1385 SYSCALL_DEFINE4(openat, ...) (arch entry-point wrappers map back)
find_definition("trace_sched_switch") → include/trace/events/sched.h:220 TRACE_EVENT(sched_switch, ...)
find_definition("css_set_lock") → kernel/cgroup/cgroup.c:82 DEFINE_SPINLOCK(css_set_lock);Covered families: SYSCALL_DEFINE0..6 / COMPAT_SYSCALL_DEFINE* (incl.
__x64_/__ia32_/__arm64_/__se_/__do_ wrapper spellings), tracepoints
(TRACE_EVENT, DEFINE_EVENT, DECLARE_TRACE, ...), and the bare-name
definers (DEFINE_SPINLOCK, DEFINE_MUTEX, DEFINE_PER_CPU*,
DECLARE_BITMAP, module_param*, any DEFINE_/DECLARE_-shaped macro) —
plus a last-resort fuzzy tier that tries underscore-variant spellings.
Resolved results are flagged with resolved_via in the envelope and ranked
ahead of same-named textual shadows; DEFINE_* sites rank above their
DECLARE_* counterparts. find_definition additionally reports the
EXPORT_SYMBOL / EXPORT_SYMBOL_GPL variant a kernel symbol is exported
with, in an exported field. Costs nothing when a symbol resolves normally
(only family-shaped names like sys_*/trace_* get the extra indexed
lookups); disable with --no-macro-resolve, GTAGS_MCP_MACRO_RESOLVE=0, or
macro_resolve = false.
What gets indexed (junk stays out)
Indexing feeds gtags an explicit file list instead of letting it walk the tree:
In a git repository the list comes from
git ls-files—.gitignoreis respected exactly, so build output, vendored blobs, and generated files never pollute the index. Disable withrespect_gitignore = falsein.gtags-mcp.toml.Outside git, the tree is walked minus well-known junk directories (
.git,node_modules,build,dist,.venv, ...).skip_globsin.gtags-mcp.tomldrops anything else you never want indexed.
Incremental refreshes recollect the list, so newly ignored files drop out of the index and new files appear — automatically.
Multi-language projects (C + Python + more)
Real projects mix languages — a C core with Python tooling, JS frontends, Go services. The server handles this automatically:
Native languages (C, C++, Java, PHP, Yacc, assembly) use GNU Global's fast built-in parser.
Everything else (Python, Go, Rust, JavaScript, TypeScript, Ruby, ... ~150 languages) is indexed through Global's ctags + Pygments plugin parsers — same index, same tools, same queries.
The one-line installer (and mcp-gtags-server setup) enables this automatically — it installs universal-ctags and Pygments into user space, and the server switches to the native-pygments parser label on its own. Prefer system packages? Those work too:
sudo apt install exuberant-ctags python3-pygments # Debian/Ubuntu
sudo dnf install ctags python3-pygments # Fedora
brew install ctags && pip install pygments # macOSNow find_definition("py_util"), get_symbol_body (indentation-aware for Python), find_callees, find_callers — all work across every language in the tree, in one index.
Force a specific parser label with --label, GTAGS_MCP_LABEL, or label in .gtags-mcp.toml (e.g. default for native-only, pygments for plugin-everything).
Honest caveats: for plugin-parsed languages, definitions are as accurate as ctags, but references are token-based — every occurrence of the name counts, without C-grade semantic reference tracking or local-scope awareness. For C/C++ nothing changes: the native parser still does that part.
How it works
Architecture
One long-lived server process per IDE window (stdio) — or one shared HTTP server for the whole machine. Everything heavy lives on disk and is shared: the toolchain installs itself once per machine, the index builds itself once per repo.
flowchart LR
subgraph clients["MCP clients"]
CC["Claude Code / Cursor / VS Code"]
CD["Claude Desktop (.mcpb)"]
end
subgraph proc["mcp-gtags-server — spawned once per window, lives for the session"]
T["11 navigation tools"]
RR["root resolution<br/>project_root → env → config → client roots → cwd"]
end
subgraph disk["your machine, user space (no sudo)"]
TC["~/.gtags-mcp<br/>global · gtags · ctags · Pygments<br/>self-installs on first use"]
IX["<repo>/.gtags-mcp/GTAGS<br/>one index per repo, auto-built"]
end
CC -- "stdio (uvx)" --> T
CD -- "stdio" --> T
T --> RR
RR -- "runs global (ms, indexed)" --> TC
TC -- "B-tree lookup" --> IXA tool call, end to end
The binary is not re-executed per call — the server process stays alive; each call is one JSON-RPC message plus one millisecond-scale global subprocess:
sequenceDiagram
participant A as Agent
participant S as server (long-lived process)
participant G as global (GTAGS index)
A->>S: find_definition("tcp_v4_rcv")
Note over S: first call on this machine?<br/>toolchain installs itself in the background,<br/>calls answer "installing — retry shortly"
Note over S: first query in this repo?<br/>index auto-builds once (~66 s for the kernel)
S->>G: global -dx tcp_v4_rcv
G-->>S: net/ipv4/tcp_ipv4.c:2067 (milliseconds)
S-->>A: narrow JSON answer — no grep firehoseFirst query on a tree? The index is built automatically (the only operation that ever blocks — and only once).
Where do the index files go? Into a single
.gtags-mcp/folder at the project root — never loose files next to your code. The folder ships its own.gitignore, sogit statusstays clean without touching yours. A pre-existing root-levelGTAGS(from older versions, or your own gtags runs) keeps being used as-is.mcp-gtags-server doctorshows the location.Files changed? A debounced incremental refresh runs in the background: queries always answer instantly from the current index while
gtags -icatches up behind the scenes. Measured on the kernel: queries return in 0.02s while the 25s freshness check runs invisibly. Staleness is bounded by the debounce window; callupdate_indexfor a synchronous, guaranteed-fresh barrier right after edits.Huge result? Pagination footers tell the agent exactly how to fetch the next page — or the tool itself suggests a narrower one (
find_callerson a symbol used in 500+ files points tofind_references, which groups by file).
FAQ
Why gtags instead of a language server (LSP)?
LSP servers give richer semantics but need a working build configuration, per-editor setup, and serious warm-up time on large trees. gtags indexes 37M lines in about a minute with zero configuration, handles the kernel-scale codebases LSPs choke on, and its fuzzy parser doesn't care whether the code currently compiles. For C/C++ navigation questions — definition, references, callers — it's the pragmatic sweet spot. (Wrapping clangd for the compile-DB case was considered and deliberately rejected: users who have a working compile_commands.json already have clangd and its ecosystem — see ROADMAP.md.)
What languages? C, C++, Yacc, Java, PHP, and assembly natively — plus Python, Go, Rust, JS/TS, Ruby, and ~150 others via the ctags/Pygments plugin parsers (see Multi-language projects).
Does the agent have to manage the index?
No. That's the point. Build-on-first-query, background refresh with adaptive debounce, zero blocking — queries never wait for index maintenance. The explicit update_index tool exists only as an escape hatch: a synchronous freshness barrier after edits, and a from-scratch rebuild with full=true.
Where does the index live? Can I delete it?
In .gtags-mcp/ at the project root (self-gitignored). Delete it freely any time — the next query rebuilds it from scratch.
Will it fight my agent's built-in tools? The tool descriptions are written to steer the model: they say when to use indexed lookups instead of grep. In practice agents pick the faster, narrower tool naturally.
ModuleNotFoundError: No module named 'mcp.server.fastmcp' on startup?
Releases up to v1.4.2 declared an uncapped mcp dependency, so installs made after the MCP Python SDK 2.0 release resolve an SDK they can't import. v1.4.3 fixes it by capping the SDK below 2.0, and v1.5.0+ runs natively on SDK 2.x (mcp>=2.2,<3), speaking every protocol revision from 2024-11-05 through 2026-07-28. Pick up the fix with uvx --refresh mcp-gtags-server --help (or uvx mcp-gtags-server@latest), or re-run the installer. If you must stay on an older release, pin the SDK yourself: uvx --with 'mcp<2' mcp-gtags-server@1.4.2.
Development
git clone https://github.com/harshithsunku/mcp-gtags-server
cd mcp-gtags-server
uv run --extra dev pytest # 325 tests; e2e tests auto-skip if GNU Global is absent
npx @modelcontextprotocol/inspector mcp-gtags-server # poke at it interactivelyTests build a real C project in a temp dir and exercise auto-indexing, auto-refresh, caller mapping, body extraction, pagination, user-space binary discovery, and config layering end-to-end.
Correctness is also measured, not assumed: mcp-gtags-server eval --golden evals/golden.jsonl --root <kernel-tree> runs a 64-case golden set covering all 8 tools (definitions, macro resolution, references, callers, callees, bodies, guards, reachability, maintenance) against a real kernel and prints recall / precision@1 — CI does this weekly against a pinned tag. See docs/capability.md for the current numbers.
Release flow: bump version in pyproject.toml, tag vX.Y.Z, push — CI publishes to PyPI and users pick the update up on their next installer re-run. Prebuilt GNU Global binaries are rebuilt by tagging global-v<version> (or gh workflow run release-binaries.yml -f version=<version> to replace the assets of an existing release in place); Linux builds run inside manylinux_2_28 containers so they work on any glibc ≥ 2.28 host, enforced by a CI symbol-ceiling check.
Roadmap
See ROADMAP.md — structured JSON output landed in v0.8.0, ctags
metadata enrichment (kind/signature/scope) in v0.8.1, #ifdef/config-guard
awareness (the headline capability for kernel and firmware trees) in v0.9.0,
then, all shipped in v1.0.0: macro-family symbol resolution (sys_read → its
SYSCALL_DEFINE3 site), the agent workflow tools (reachability,
diff-impact analysis), automatic recovery from corrupted index databases, and the
correctness eval harness — a 64-case golden set against pinned kernel v6.16
scoring 100% recall / 100% precision@1 in CI, with the measured writeup in
docs/capability.md. Every technical milestone is done;
what remains is distribution (MCP registry, directories, the writeup post).
Contributions welcome — open an issue or PR.
License
MIT © Harshith Sunku
Available Tools
8 toolsfind_calleesFind calleesARead-only
What does this function call? Callees / outgoing calls of a function.
Shows a function's dependencies without reading any file: call sites are detected in its body and verified against the index, split into in-tree functions (with locations) and external/unresolved names. Macro-generated and parser-missed (EXPORT_SYMBOL-recovered) definitions resolve too, flagged resolved_via. JSON results: {in_tree: [{symbol, path, line}], external: [names]}.
Args: symbol: Exact name of the function to analyze.
| Name | Required | Description | Default |
|---|---|---|---|
| format | No | text | |
| symbol | Yes | ||
| project_root | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Beyond the readOnly/openWorld annotations, it discloses index-based detection, the in-tree/external split, handling of macro-generated and parser-missed definitions via resolved_via, and the JSON result shape. This is substantial behavioral context.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is information-dense but well organized: a short defining question, a concise behavior summary, and a compact output schema sketch. Every sentence adds value and there is no padding.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With no output schema, the description compensates by giving the JSON return shape and key edge cases. However, it omits semantics for format and project_root, and the default-text vs JSON-message ambiguity leaves a minor but real gap.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, so the description must compensate, but it only clarifies symbol as the exact name. The format parameter (json/text with text as default) and project_root are not explained, and the 'JSON results' example is not reconciled with the default text format.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states the exact scope: callees/outgoing calls of a function, and further clarifies what the output contains (in-tree locations vs external names). It clearly distinguishes this from siblings like find_callers and find_references without needing to compare schemas.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description gives clear invocation context: pass an exact function symbol, and the tool uses the index rather than reading files. It does not explicitly name alternatives or say when not to use this tool, so it stops short of a 5.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
find_callersFind callersARead-only
Who calls this function? Callers / call hierarchy / incoming calls, deduplicated per calling function with call counts.
Each reference is mapped to its enclosing function and shown with the source line of its first call site, so there is nothing to re-grep. The highest signal-to-noise "who uses this?" view for impact analysis; iterate it to walk the caller graph upward. JSON results: {caller, path, sites, call} items (call = source of the first site).
Args: symbol: Exact symbol name whose callers you want.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | ||
| format | No | text | |
| offset | No | ||
| symbol | Yes | ||
| project_root | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true and openWorldHint=false, so the safety profile is covered. The description adds valuable behavioral context: deduplication per calling function, mapping references to enclosing functions, showing the source line of the first call site, and the JSON result shape. This goes beyond what annotations provide.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is well-structured and front-loaded with the core purpose. It includes a compact JSON result example and an Args section. Slightly verbose in the middle ('so there is nothing to re-grep') but that sentence earns its place by explaining a key benefit. Overall efficient and readable.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a read-only query tool with no output schema, the description covers the main use case, result shape, and key parameter. It lacks explicit guidance on pagination parameters (limit/offset) and the text format option, but the core calling scenario is well covered. The absence of an output schema is partially compensated by the inline JSON example.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, so the description must compensate. It documents the key parameter 'symbol' ('Exact symbol name whose callers you want') and mentions the JSON result fields. However, it doesn't explain limit, offset, format, or project_root semantics beyond what the schema already provides. The description adds some value for the main parameter but leaves the other four undocumented.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool's purpose: find callers of a function, deduplicated per calling function with call counts. It distinguishes itself from siblings by explicitly positioning itself as the 'who uses this?' view for impact analysis and mentions walking the caller graph upward, which differentiates it from find_references and find_callees.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description explains when to use this tool ('highest signal-to-noise who uses this? view for impact analysis') and how to iterate it ('walk the caller graph upward'). It doesn't explicitly name alternatives or say when not to use it, but the context is clear enough for an agent to select it over siblings like find_references or find_callees.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
find_definitionFind definitionARead-only
Go to definition: where a C/C++ symbol (function, struct, macro, typedef, enum) is defined, with a usage summary — the best first query.
Each definition carries its #if/#ifdef guard stack (several guarded definitions = a config choice) and ctags kind/signature. The summary gives reference and file counts, the hottest files and the EXPORT_SYMBOL* variant. Macro-generated symbols resolve ("sys_read" -> SYSCALL_DEFINE3(read, ...), DEFINE_SPINLOCK names), and definitions the index parser missed are recovered from their EXPORT_SYMBOL* site — both flagged resolved_via. A miss suggests similarly named symbols.
JSON: results are {symbol, path, line, col, kind, typeref, scope, signature, guard, snippet} records; the envelope adds definition_count, guard_variants, reference_count, file_count, top_files, exported.
Args: symbol: Exact symbol name, e.g. "tcp_v4_rcv". case_insensitive: Match ignoring case (skips the usage summary). active_config: Kernel .config path or macro list like "CONFIG_SMP,BITS_PER_LONG=64,!CONFIG_DEBUG"; drops definitions whose guard stack is definitely false under it (count reported as config_filtered). Unknown macros never drop anything.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | ||
| format | No | text | |
| offset | No | ||
| symbol | Yes | ||
| project_root | No | ||
| active_config | No | ||
| case_insensitive | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations indicate readOnlyHint=true and openWorldHint=false, but the description adds significant behavioral context: it explains how macro-generated symbols resolve, how definitions are recovered from EXPORT_SYMBOL* sites, and the flag 'resolved_via'. It also details the behavior of active_config (drops definitions with false guards, config_filtered count). This goes beyond annotations and is not contradicted.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is lengthy but information-dense and front-loaded with a summary line. It is well-organized in paragraphs and an Args section. There is some verbosity (e.g., examples inside parentheses), but each sentence adds value. A slightly tighter structure would improve it, but it is not wasteful.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool's complexity (7 params, no output schema), the description is fairly complete. It explains the result structure (envelope fields), the behavior of active_config, and the resolution of macros. However, it does not explain the 'limit', 'offset', and pagination behavior, nor the 'format' parameter, and it omits the meaning of 'guard_variants' in the envelope. These gaps are minor given the schema provides defaults but not semantics.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The schema has 0% description coverage for parameters. The description explicitly details 'symbol', 'case_insensitive', and 'active_config' parameters with examples and behaviors. However, 'limit', 'format', 'offset', and 'project_root' are not explained. Since the coverage is very low, the description compensates partially by covering the required parameter and two key optional ones, but leaves some undocumented.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool's purpose: go to definition for C/C++ symbols, and distinguishes it from siblings by noting it is 'the best first query.' It lists the symbol kinds (function, struct, macro, typedef, enum) and includes a usage summary. This is a specific and informative purpose statement.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description indicates when to use it (as a first query) and mentions that case_insensitive skips the usage summary, but it does not explicitly compare with siblings such as find_references or find_callers. It gives context for usage but lacks explicit alternatives or when-not-to-use scenarios.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
find_referencesFind referencesARead-only
Find all references / usages of a C/C++ symbol — every call and use site.
Only real reference sites from the index, each with its #if/#ifdef guard stack. Very widely used symbols (more than 200 sites, e.g. kmalloc) come back grouped by file with counts — see where usage concentrates, then narrow with path_prefix. Symbols with no in-tree definition (libc calls, some variables) work too, flagged "fallback": "symbol_usages".
Args: symbol: Exact symbol name. case_insensitive: Match ignoring case. active_config: Kernel .config path or macro list; drops references whose guard stack is definitely false under it (config_filtered). group_by: "auto" (default: per-file counts above 200 references), "line" (always individual sites) or "file" (always per-file counts). path_prefix: Only references under this directory, e.g. "fs/ext4".
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | ||
| format | No | text | |
| offset | No | ||
| symbol | Yes | ||
| group_by | No | auto | |
| path_prefix | No | ||
| project_root | No | ||
| active_config | No | ||
| case_insensitive | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Beyond the readOnlyHint/openWorldHint annotations, it discloses rich behavior: results only come from the index, each reference carries its #if/#ifdef guard stack, symbols with >200 sites are grouped per-file with counts, active_config drops definitely-false guarded references (config_filtered), and no-definition symbols produce a "fallback": "symbol_usages" flag. No contradiction with annotations — "find" is consistent with readOnlyHint=true, and index-scoped results align with openWorldHint=false.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Well-structured and front-loaded: purpose first, then behavioral caveats, then an Args list. The prose is dense with useful specifics. Minor redundancy — group_by's "auto (default: per-file counts above 200 references)" restates the 200-site grouping already explained in the opening paragraph — but this is arguably a helpful reminder rather than waste.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With 9 parameters, no output schema, and 0% schema description coverage, the description carries heavy burden and mostly succeeds: it conveys the result shape (per-file counts vs individual sites, fallback and config_filtered flags, guard stacks) and explains core parameters. The only notable gaps are the unexplained project_root parameter and no description of limit/offset pagination behavior.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, so the description must compensate — and it does for the core parameters: group_by value semantics, active_config behavior, path_prefix with a concrete example, and case_insensitive meaning. However, four parameters (limit, format, offset, project_root) are never mentioned in the Args section; project_root in particular is ambiguous for a kernel-tool agent.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The first sentence states an exact verb+resource: "Find all references / usages of a C/C++ symbol — every call and use site." This clearly delineates the tool's scope from siblings like find_callers/find_callees (directional relationships) and find_definition (definitions) by covering all call and use sites. The phrase "Only real reference sites from the index" further pins down what counts as a reference.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description gives operational guidance: narrow widely-used symbols with path_prefix, and notes that symbols without in-tree definitions (libc calls, variables) still work and are flagged as fallback. This implies when the tool is appropriate. However, it never names sibling tools or states when not to use it (e.g., "use find_callers for direct callers only"), leaving some boundary inference to the agent.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_symbol_bodyGet symbol bodyARead-only
Read a symbol's source: the full body of a function, struct or macro definition, without reading the whole file.
Extracts only the definition's lines, so a one-screen function never costs a 5000-line file read. Macro-generated and parser-missed (EXPORT_SYMBOL-recovered) definitions resolve too, flagged resolved_via. JSON results: {path, line, body} items.
Args: symbol: Exact symbol name. max_definitions: Return at most this many bodies when the symbol is multiply defined (default 3).
| Name | Required | Description | Default |
|---|---|---|---|
| format | No | text | |
| symbol | Yes | ||
| project_root | No | ||
| max_definitions | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true, and the description aligns with that (no contradiction). It adds context that the tool extracts only definition lines, handles macro-generated and parser-missed definitions via 'resolved_via', and returns JSON with {path, line, body}. This is valuable beyond annotations, though it doesn't discuss performance limits or potential edge cases.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is well-structured: it leads with a clear summary, then provides efficiency motivation, fallback behavior, and output format, before listing args. Each sentence earns its place, with no fluff. The front-loading of the core benefit (avoiding full file reads) is effective.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a read-only tool with no output schema, the description covers the essential return structure (JSON with path, line, body) and edge cases (macro-generated, max_definitions). However, it doesn't specify the default format (text vs json) or how 'resolved_via' appears, which might be minor gaps given the JSON structure is mentioned.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 0%, so the description must elaborate on parameters. It explains 'symbol' as exact symbol name and 'max_definitions' as limiting multiply-defined bodies, which is helpful. However, it omits 'format' and 'project_root' entirely, which are present in the schema; the agent may need to infer their purpose, but the schema's default values and types give some clues.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states it reads a symbol's source body without reading the whole file, differentiating it from file-level tools. It mentions specific resource (function, struct, macro) and exact behavior (extracts definitions), and implicitly distinguishes from siblings like find_definition which likely locates but doesn't read bodies.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description implicitly indicates when to use it: when you need the body of a symbol without reading the entire file. It notes macro-generated and parser-missed definitions are resolved, suggesting fallback behavior. However, it doesn't explicitly compare to alternatives like find_definition, leaving some ambiguity about when to prefer this over find_definition.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_file_symbolsList file symbolsARead-only
Outline of one source file: every function, struct and macro it defines (document symbols), with kind, signature and #ifdef guards.
Use this INSTEAD of reading a file when you only need its API surface.
Args: file_path: Source file, relative to the project root or absolute.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | ||
| format | No | text | |
| offset | No | ||
| file_path | Yes | ||
| project_root | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true, so the safety profile is covered. The description adds that the tool returns document symbols only, includes kind/signature/guards, and serves as an API-surface outline. It does not mention pagination or errors, but for a read-only listing tool this is sufficient context.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is compact and well organized: what it returns, when to use it, and the key argument. Every sentence adds value, and the most important information is front-loaded with no fluff.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
The core purpose and read-only nature are clear, and annotations cover safety. However, with no output schema and 0% parameter coverage, the agent is left to infer pagination (limit/offset), the meaning of project_root, and how format changes the result. Acceptable for a simple list tool but not fully complete.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, so the description must compensate. It only explains file_path ('relative to project root or absolute'); limit, offset, format, and project_root are left to their schema titles/defaults. project_root in particular is ambiguous in relation to relative paths, and no guidance is given for pagination or output format selection.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description opens with a specific verb ('Outline') and resource ('one source file'), then enumerates exactly what is included: functions, structs, macros, with kind, signature, and #ifdef guards. This clearly distinguishes it from sibling tools like find_definition or get_symbol_body, which target individual symbols rather than a whole file's API surface.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It explicitly says 'Use this INSTEAD of reading a file when you only need its API surface,' giving a clear trigger condition and an alternative. It does not enumerate sibling tools as alternatives, but the boundary against reading the whole file is enough to guide selection.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
reachabilityCall-path reachabilityARead-only
Call path / call chain: does FROM transitively call TO, and through which functions?
Use this instead of chaining find_callers rounds when the question is "can this function end up in that one?". BFS over the caller graph returns the SHORTEST chain, each hop with the call site's file:line. JSON results: {path_found, hops, depth, nodes_explored}; hops run from from_symbol to to_symbol. Static analysis cannot follow function pointers (ops structs, callbacks).
Args: from_symbol: The caller end ("can this reach ..."). to_symbol: The callee end ("... this function?"). max_depth: Longest chain to consider, in calls (1-12, default 8).
| Name | Required | Description | Default |
|---|---|---|---|
| format | No | text | |
| max_depth | No | ||
| to_symbol | Yes | ||
| from_symbol | Yes | ||
| project_root | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
The description adds substantial behavior beyond the annotations: BFS over the caller graph, shortest-chain semantics, per-hop call-site file:line details, result fields, hop direction, and the limitation that function pointers cannot be followed. This is exactly the kind of context an agent needs.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is efficient and front-loaded, opening with the core question and then adding usage, behavior, limitations, and parameter details. No sentence is wasted, and the argument explanations are compact and meaningful.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
The description is largely complete: it covers purpose, alternative usage, output shape, direction of hops, depth limits, and a key limitation. It does not mention the text output format or project_root, but the required behavior and main arguments are fully specified, so the gaps are minor.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, so the description carries the burden. It usefully explains from_symbol, to_symbol, and max_depth, but it does not explain format or project_root, both of which appear in the schema. The main parameters are well covered, but the full parameter set is not.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states a specific verb and resource: it determines whether FROM transitively calls TO and identifies the chain of functions. It also explicitly differentiates itself from find_callers, and the 'shortest chain' detail sharpens the scope.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It gives explicit guidance: use this instead of chaining find_callers rounds when the question is whether one function can end up in another. This directly routes the agent to the correct sibling and explains the intended use case.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
update_indexUpdate indexAIdempotent
Refresh the code index synchronously after editing files — the guaranteed-freshness barrier.
Query tools refresh the index automatically in the background, so results can lag very recent edits by a few seconds. Call this right after editing files when the very next query must see the changes.
Args: full: Rebuild the index from scratch instead of refreshing it incrementally (rarely needed — large branch switch, suspected corruption).
| Name | Required | Description | Default |
|---|---|---|---|
| full | No | ||
| format | No | text | |
| project_root | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already indicate that this operation is not read-only, is idempotent, and is not destructive. The description adds context that the refresh is synchronous and can be a performance barrier, which is useful. However, it does not disclose any side effects on the index or the specific behavior when 'full' is true beyond a brief note. Since annotations already cover the safety profile (not read-only, idempotent, not destructive), a 3 is appropriate – the description adds some behavioral context but not extensive detail.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is concise and well-structured. It opens with a clear purpose, provides usage context, and then details the 'full' parameter in a list format. It avoids unnecessary fluff and stays focused on guiding the agent. The only minor inefficiency is that the parameter explanation is slightly repetitive with the schema default, but overall it earns a high score.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For an index refresh tool with three parameters and no output schema, the description covers the primary use case well but leaves gaps. The tool's side effects on the index are not deeply described, and two parameters ('format' and 'project_root') are not explained. Given that the tool is relatively simple and annotations provide safety info, the description is adequate but not fully complete. A 3 reflects that there are improvement opportunities, but it is not severely lacking.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The schema description coverage is 0%, so the description must carry the burden of explaining parameters. The description explains the 'full' parameter's purpose ('rebuild from scratch... rarely needed') and gives scenarios like 'large branch switch, suspected corruption.' However, 'format' and 'project_root' are not mentioned in the description; their purpose is not covered. Since schema also lacks descriptions, this leaves a gap. While the main parameter is well-handled, the others are undocumented.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool's purpose: to refresh the code index synchronously after editing files. It uses a specific verb ('refresh') and a specific resource ('code index'), making it distinct from the sibling query tools listed. It also highlights its role as a 'guaranteed-freshness barrier', which distinguishes it from the background-refresh behavior of query tools.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description explicitly states when to use this tool: 'right after editing files when the very next query must see the changes.' It also contrasts with query tools that refresh automatically in the background, implying that the agent should use this tool instead of relying on automatic refreshes when immediacy is critical. It does not list alternatives by name but provides clear context for when to choose this tool.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
11 tool updates
v2.0.0- Removed
blast_radius - Changed
find_callees2 fields changed- changed
Input schema / properties / format / defaultPrevious value: -"json"New value: +"text" - changed
Output schema / (root)Previous value: -{ - "properties": { - "result": { - "title": "Result", - "type": "string" - } - }, - "required": [ - "result" - ], - "title": "find_calleesOutput", - "type": "object" -}New value: +null
- Changed
find_callers2 fields changed- changed
Input schema / properties / format / defaultPrevious value: -"json"New value: +"text" - changed
Output schema / (root)Previous value: -{ - "properties": { - "result": { - "title": "Result", - "type": "string" - } - }, - "required": [ - "result" - ], - "title": "find_callersOutput", - "type": "object" -}New value: +null
- Changed
find_definition2 fields changed- changed
Input schema / properties / format / defaultPrevious value: -"json"New value: +"text" - changed
Output schema / (root)Previous value: -{ - "properties": { - "result": { - "title": "Result", - "type": "string" - } - }, - "required": [ - "result" - ], - "title": "find_definitionOutput", - "type": "object" -}New value: +null
- Changed
find_references4 fields changed- changed
Input schema / properties / format / defaultPrevious value: -"json"New value: +"text" - added
Input schema / properties / group_byAdded value: +{ + "default": "auto", + "enum": [ + "auto", + "line", + "file" + ], + "title": "Group By", + "type": "string" +} - added
Input schema / properties / path_prefixAdded value: +{ + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Path Prefix" +} - changed
Output schema / (root)Previous value: -{ - "properties": { - "result": { - "title": "Result", - "type": "string" - } - }, - "required": [ - "result" - ], - "title": "find_referencesOutput", - "type": "object" -}New value: +null
- Changed
get_symbol_body2 fields changed- changed
Input schema / properties / format / defaultPrevious value: -"json"New value: +"text" - changed
Output schema / (root)Previous value: -{ - "properties": { - "result": { - "title": "Result", - "type": "string" - } - }, - "required": [ - "result" - ], - "title": "get_symbol_bodyOutput", - "type": "object" -}New value: +null
- Changed
list_file_symbols2 fields changed- changed
Input schema / properties / format / defaultPrevious value: -"json"New value: +"text" - changed
Output schema / (root)Previous value: -{ - "properties": { - "result": { - "title": "Result", - "type": "string" - } - }, - "required": [ - "result" - ], - "title": "list_file_symbolsOutput", - "type": "object" -}New value: +null
- Changed
reachability2 fields changed- changed
Input schema / properties / format / defaultPrevious value: -"json"New value: +"text" - changed
Output schema / (root)Previous value: -{ - "properties": { - "result": { - "title": "Result", - "type": "string" - } - }, - "required": [ - "result" - ], - "title": "reachabilityOutput", - "type": "object" -}New value: +null
- Removed
summarize_references - Removed
symbol_info - Changed
update_index2 fields changed- changed
Input schema / properties / format / defaultPrevious value: -"json"New value: +"text" - changed
Output schema / (root)Previous value: -{ - "properties": { - "result": { - "title": "Result", - "type": "string" - } - }, - "required": [ - "result" - ], - "title": "update_indexOutput", - "type": "object" -}New value: +null
10 tool updates
v1.4.1- Removed
call_hierarchy - Removed
complete_symbol - Removed
find_dead_symbols - Removed
find_files - Removed
find_includers - Removed
find_symbol_usages - Removed
grep_project - Removed
index_project - Removed
project_overview - Changed
update_index1 field changed- added
Input schema / properties / fullAdded value: +{ + "default": false, + "title": "Full", + "type": "boolean" +}
5 tool updates
v1.1.0- Added
blast_radius - Changed
find_definition1 field changed- added
Input schema / properties / active_configAdded value: +{ + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Active Config" +}
- Changed
find_references1 field changed- added
Input schema / properties / active_configAdded value: +{ + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Active Config" +}
- Added
reachability - Changed
symbol_info1 field changed- added
Input schema / properties / active_configAdded value: +{ + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Active Config" +}
18 tool updates
v0.8.2- Changed
call_hierarchy1 field changed- added
Input schema / properties / formatAdded value: +{ + "default": "json", + "enum": [ + "json", + "text" + ], + "title": "Format", + "type": "string" +}
- Changed
complete_symbol1 field changed- added
Input schema / properties / formatAdded value: +{ + "default": "json", + "enum": [ + "json", + "text" + ], + "title": "Format", + "type": "string" +}
- Changed
find_callees1 field changed- added
Input schema / properties / formatAdded value: +{ + "default": "json", + "enum": [ + "json", + "text" + ], + "title": "Format", + "type": "string" +}
- Changed
find_callers1 field changed- added
Input schema / properties / formatAdded value: +{ + "default": "json", + "enum": [ + "json", + "text" + ], + "title": "Format", + "type": "string" +}
- Changed
find_dead_symbols1 field changed- added
Input schema / properties / formatAdded value: +{ + "default": "json", + "enum": [ + "json", + "text" + ], + "title": "Format", + "type": "string" +}
- Changed
find_definition1 field changed- added
Input schema / properties / formatAdded value: +{ + "default": "json", + "enum": [ + "json", + "text" + ], + "title": "Format", + "type": "string" +}
- Changed
find_files1 field changed- added
Input schema / properties / formatAdded value: +{ + "default": "json", + "enum": [ + "json", + "text" + ], + "title": "Format", + "type": "string" +}
- Changed
find_includers1 field changed- added
Input schema / properties / formatAdded value: +{ + "default": "json", + "enum": [ + "json", + "text" + ], + "title": "Format", + "type": "string" +}
- Changed
find_references1 field changed- added
Input schema / properties / formatAdded value: +{ + "default": "json", + "enum": [ + "json", + "text" + ], + "title": "Format", + "type": "string" +}
- Changed
find_symbol_usages1 field changed- added
Input schema / properties / formatAdded value: +{ + "default": "json", + "enum": [ + "json", + "text" + ], + "title": "Format", + "type": "string" +}
- Changed
get_symbol_body1 field changed- added
Input schema / properties / formatAdded value: +{ + "default": "json", + "enum": [ + "json", + "text" + ], + "title": "Format", + "type": "string" +}
- Changed
grep_project1 field changed- added
Input schema / properties / formatAdded value: +{ + "default": "json", + "enum": [ + "json", + "text" + ], + "title": "Format", + "type": "string" +}
- Changed
index_project1 field changed- added
Input schema / properties / formatAdded value: +{ + "default": "json", + "enum": [ + "json", + "text" + ], + "title": "Format", + "type": "string" +}
- Changed
list_file_symbols1 field changed- added
Input schema / properties / formatAdded value: +{ + "default": "json", + "enum": [ + "json", + "text" + ], + "title": "Format", + "type": "string" +}
- Changed
project_overview1 field changed- added
Input schema / properties / formatAdded value: +{ + "default": "json", + "enum": [ + "json", + "text" + ], + "title": "Format", + "type": "string" +}
- Changed
summarize_references1 field changed- added
Input schema / properties / formatAdded value: +{ + "default": "json", + "enum": [ + "json", + "text" + ], + "title": "Format", + "type": "string" +}
- Changed
symbol_info1 field changed- added
Input schema / properties / formatAdded value: +{ + "default": "json", + "enum": [ + "json", + "text" + ], + "title": "Format", + "type": "string" +}
- Changed
update_index1 field changed- added
Input schema / properties / formatAdded value: +{ + "default": "json", + "enum": [ + "json", + "text" + ], + "title": "Format", + "type": "string" +}
18 tool updates
v0.4.0- First observed
call_hierarchy - First observed
complete_symbol - First observed
find_callees - First observed
find_callers - First observed
find_dead_symbols - First observed
find_definition - First observed
find_files - First observed
find_includers - First observed
find_references - First observed
find_symbol_usages - First observed
get_symbol_body - First observed
grep_project - First observed
index_project - First observed
list_file_symbols - First observed
project_overview - First observed
summarize_references - First observed
symbol_info - First observed
update_index
TDQS
Scored across 8 tools
Most tools have clearly distinct purposes — definition lookup, reference enumeration, caller/callee analysis, body extraction, file outline, and reachability — and the verbose descriptions reinforce those boundaries. However, find_callers and find_references both surface call sites (one deduplicated by enclosing function, one listing every use), and find_definition overlaps with get_symbol_body enough that an agent could misselect without reading closely.
Four tools follow a clean find_<plural-noun> pattern (find_callers, find_definition, find_references, find_callees), and list_file_symbols, get_symbol_body, and update_index all use verb_noun as well. The bare noun 'reachability' breaks the pattern, and the mix of 'list'/'get'/'find' verbs is slightly varied, but the overall convention is predictable and readable.
Eight tools is well-scoped for a C/C++ code navigation server. Each tool provides a distinct index query — definition, references, callers, callees, call path, symbol body, file outline, and index refresh — with no apparent redundancy, so every tool earns its place.
The surface covers the core navigation lifecycle thoroughly: definition resolution, reference enumeration, incoming/outgoing call analysis, transitive reachability, body extraction, file-level outlines, and index maintenance. The notable gap is that symbol lookup requires exact names — there is no prefix/fuzzy search for similarly named symbols, even though find_definition's description hints at such a fallback rather than providing a tool for it.
Maintenance
Related MCP Connectors
Code intelligence for coding agents: semantic, AST, graph, and full-text search. 279+ languages.
Codebase graphs, caller impact analysis, and recorded project context for AI coding agents.
Shared memory for coding agents. Stop re-explaining your codebase every session.
Search indexed code, trace dependencies, assess change impact, and recall repository memory.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceA minimalist indexing tool that provides AI agents with semantic search and structural AST parsing for deep codebase understanding. It enables autonomous agents to navigate large codebases predictably using vector embeddings and native language server capabilities like definition and reference tracking.-
- AlicenseNot gradedqualityCmaintenanceProvides IDE-like code navigation and search for local repositories, enabling AI assistants to perform symbol search, trigram indexing, and semantic navigation.AGPL 3.0
- AlicenseNot gradedqualityDmaintenanceExposes type-aware code navigation and fast file search to AI agents via language servers, enabling definitions, references, symbols, and file lookup without reading entire codebases.3,050 npmMIT
- AlicenseNot gradedqualityCmaintenanceEnables LLM agents to efficiently understand and navigate a codebase by providing semantic search over symbols and a reference graph, replacing expensive grep/glob calls with structured tools like definition lookup, caller/callee queries, and change-impact analysis.3MIT